diff --git a/package-lock.json b/package-lock.json index 69fe852368..2dbf02db7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5626,6 +5626,126 @@ "node": ">=8" } }, + "node_modules/@microsoft/api-extractor": { + "version": "7.52.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.4.tgz", + "integrity": "sha512-mIEcqgx877CFwNrTuCdPnlIGak8FjlayZb8sSBwWXX+i4gxkZRpMsb5BQcFW3v1puuJB3jYMqQ08kyAc4Vldhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor-model": "7.30.5", + "@microsoft/tsdoc": "~0.15.1", + "@microsoft/tsdoc-config": "~0.17.1", + "@rushstack/node-core-library": "5.13.0", + "@rushstack/rig-package": "0.5.3", + "@rushstack/terminal": "0.15.2", + "@rushstack/ts-command-line": "4.23.7", + "lodash": "~4.17.15", + "minimatch": "~3.0.3", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "source-map": "~0.6.1", + "typescript": "5.8.2" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "node_modules/@microsoft/api-extractor-model": { + "version": "7.30.5", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.5.tgz", + "integrity": "sha512-0ic4rcbcDZHz833RaTZWTGu+NpNgrxVNjVaor0ZDUymfDFzjA/Uuk8hYziIUIOEOSTfmIQqyzVwlzxZxPe7tOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "~0.15.1", + "@microsoft/tsdoc-config": "~0.17.1", + "@rushstack/node-core-library": "5.13.0" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.1.tgz", + "integrity": "sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.15.1", + "ajv": "~8.12.0", + "jju": "~1.4.0", + "resolve": "~1.22.2" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/@mongodb-js/compass-components": { "version": "1.31.0", "resolved": "https://registry.npmjs.org/@mongodb-js/compass-components/-/compass-components-1.31.0.tgz", @@ -8440,6 +8560,163 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/@rushstack/node-core-library": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.13.0.tgz", + "integrity": "sha512-IGVhy+JgUacAdCGXKUrRhwHMTzqhWwZUI+qEPcdzsb80heOw0QPbhhoVsoiMF7Klp8eYsp7hzpScMXmOa3Uhfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "~8.13.0", + "ajv-draft-04": "~1.0.0", + "ajv-formats": "~3.0.1", + "fs-extra": "~11.3.0", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/ajv": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", + "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/node-core-library/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/rig-package": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.3.tgz", + "integrity": "sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "node_modules/@rushstack/terminal": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.2.tgz", + "integrity": "sha512-7Hmc0ysK5077R/IkLS9hYu0QuNafm+TbZbtYVzCMbeOdMjaRboLKrhryjwZSRJGJzu+TV1ON7qZHeqf58XfLpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/node-core-library": "5.13.0", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/terminal/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.23.7", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.7.tgz", + "integrity": "sha512-Gr9cB7DGe6uz5vq2wdr89WbVDKz0UeuFEn5H2CfWDe7JvjFFaiV15gi6mqDBTbHhHCWS7w8mF1h3BnIfUndqdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/terminal": "0.15.2", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, "node_modules/@segment/analytics-core": { "version": "1.4.1", "license": "MIT", @@ -9732,6 +10009,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -16292,7 +16576,9 @@ "license": "MIT" }, "node_modules/fs-extra": { - "version": "11.2.0", + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -17635,6 +17921,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/import-local": { "version": "3.1.0", "devOptional": true, @@ -18626,6 +18922,13 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true, + "license": "MIT" + }, "node_modules/jmespath": { "version": "0.16.0", "license": "Apache-2.0", @@ -26575,6 +26878,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "license": "MIT", @@ -27571,7 +27884,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -30012,6 +30327,8 @@ "version": "3.10.0", "license": "Apache-2.0", "dependencies": { + "@babel/core": "^7.26.10", + "@babel/types": "^7.26.10", "@mongosh/arg-parser": "3.10.0", "@mongosh/errors": "2.4.0", "@mongosh/history": "2.4.6", @@ -30020,6 +30337,7 @@ "mongodb-redact": "^1.1.5" }, "devDependencies": { + "@microsoft/api-extractor": "^7.39.3", "@mongodb-js/eslint-config-mongosh": "^1.0.0", "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-mongosh": "^1.0.0", diff --git a/packages/autocomplete/src/index.spec.ts b/packages/autocomplete/src/index.spec.ts index f12bd2bd17..118e27bc7e 100644 --- a/packages/autocomplete/src/index.spec.ts +++ b/packages/autocomplete/src/index.spec.ts @@ -294,7 +294,7 @@ describe('completer.completer', function () { it('returns all suggestions', async function () { const i = 'db.'; - const attr = shellSignatures.Database.attributes as any; + const attr = shellSignatures.DatabaseImpl.attributes as any; const dbComplete = Object.keys(attr); const adjusted = dbComplete .filter((c) => !attr[c].deprecated) @@ -347,7 +347,7 @@ describe('completer.completer', function () { it('returns all suggestions', async function () { const i = 'db.shipwrecks.'; const collComplete = Object.keys( - shellSignatures.Collection.attributes as any + shellSignatures.CollectionImpl.attributes as any ); const adjusted = collComplete .filter( diff --git a/packages/autocomplete/src/index.ts b/packages/autocomplete/src/index.ts index 1b6f86be4f..e7a51314c9 100644 --- a/packages/autocomplete/src/index.ts +++ b/packages/autocomplete/src/index.ts @@ -84,9 +84,9 @@ async function completer( ): Promise<[string[], string, 'exclusive'] | [string[], string]> { const SHELL_COMPLETIONS = shellSignatures.ShellApi .attributes as TypeSignatureAttributes; - const COLL_COMPLETIONS = shellSignatures.Collection + const COLL_COMPLETIONS = shellSignatures.CollectionImpl .attributes as TypeSignatureAttributes; - const DB_COMPLETIONS = shellSignatures.Database + const DB_COMPLETIONS = shellSignatures.DatabaseImpl .attributes as TypeSignatureAttributes; const AGG_CURSOR_COMPLETIONS = shellSignatures.AggregationCursor .attributes as TypeSignatureAttributes; diff --git a/packages/build/src/packaging/package/helpers.ts b/packages/build/src/packaging/package/helpers.ts index 352032c70d..556aaf900c 100644 --- a/packages/build/src/packaging/package/helpers.ts +++ b/packages/build/src/packaging/package/helpers.ts @@ -13,7 +13,9 @@ export async function execFile( ): Promise> { const joinedCommand = [args[0], ...(args[1] ?? [])].join(' '); console.info( - 'Running "' + joinedCommand + '" in ' + args[2]?.cwd ?? process.cwd() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TS2869 Right operand of ?? is unreachable because the left operand is never nullish. + `Running "${joinedCommand}" in ${args[2]?.cwd ?? process.cwd()}` ); const result = await execFileWithoutLogging(...args); console.info('"' + joinedCommand + '" resulted in:', { diff --git a/packages/cli-repl/src/cli-repl.spec.ts b/packages/cli-repl/src/cli-repl.spec.ts index 20bec91c48..2741d30ab0 100644 --- a/packages/cli-repl/src/cli-repl.spec.ts +++ b/packages/cli-repl/src/cli-repl.spec.ts @@ -1667,7 +1667,7 @@ describe('CliRepl', function () { ) .flat(); expect(apiEvents).to.have.lengthOf(1); - expect(apiEvents[0].properties.class).to.equal('Database'); + expect(apiEvents[0].properties.class).to.equal('DatabaseImpl'); expect(apiEvents[0].properties.method).to.equal( 'printShardingStatus' ); @@ -1738,8 +1738,8 @@ describe('CliRepl', function () { e.properties.count, ]) ).to.deep.equal([ - ['Database', 'hello', 2], - ['Database', 'hello', 1], + ['DatabaseImpl', 'hello', 2], + ['DatabaseImpl', 'hello', 1], ]); }); @@ -2466,8 +2466,8 @@ describe('CliRepl', function () { }); afterEach(async function () { - expect(output).not.to.include('Tab completion error'); - expect(output).not.to.include( + expect(output, output).not.to.include('Tab completion error'); + expect(output, output).not.to.include( 'listCollections requires authentication' ); await cliRepl.mongoshRepl.close(); diff --git a/packages/cli-repl/src/mongosh-repl.spec.ts b/packages/cli-repl/src/mongosh-repl.spec.ts index 3227fdced9..54cd324fd8 100644 --- a/packages/cli-repl/src/mongosh-repl.spec.ts +++ b/packages/cli-repl/src/mongosh-repl.spec.ts @@ -2,7 +2,7 @@ import { MongoshCommandFailed } from '@mongosh/errors'; import type { ServiceProvider } from '@mongosh/service-provider-core'; import { bson } from '@mongosh/service-provider-core'; -import { ADMIN_DB } from '@mongosh/shell-api/lib/enums'; +import { ADMIN_DB } from '../../shell-api/lib/enums'; import { CliUserConfig } from '@mongosh/types'; import { EventEmitter, once } from 'events'; import path from 'path'; diff --git a/packages/shell-api/api-extractor.json b/packages/shell-api/api-extractor.json new file mode 100644 index 0000000000..4bdcfef40b --- /dev/null +++ b/packages/shell-api/api-extractor.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "/lib/api.d.ts", + "apiReport": { + "enabled": false + }, + "docModel": { + "enabled": false + }, + "bundledPackages": [ + "@mongodb-js/devtools-connect", + "@mongodb-js/devtools-proxy-support", + "@mongodb-js/oidc-plugin", + "@mongosh/arg-parser", + "@mongosh/service-provider-core", + "@mongosh/types", + "mongodb", + "bson" + ], + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "", + "publicTrimmedFilePath": "/lib/api-raw.d.ts" + }, + "tsdocMetadata": { + "enabled": false + }, + "newlineKind": "lf", + "messages": { + "compilerMessageReporting": { + "default": { + "logLevel": "error" + } + }, + "extractorMessageReporting": { + "default": { + "logLevel": "error" + }, + "ae-internal-missing-underscore": { + "logLevel": "none", + "addToApiReportFile": false + }, + "ae-forgotten-export": { + "logLevel": "warning", + "addToApiReportFile": false + }, + "ae-missing-release-tag": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "none" + } + } + } +} diff --git a/packages/shell-api/bin/api-postprocess.ts b/packages/shell-api/bin/api-postprocess.ts new file mode 100644 index 0000000000..d9d3dbf3f5 --- /dev/null +++ b/packages/shell-api/bin/api-postprocess.ts @@ -0,0 +1,157 @@ +import * as babel from '@babel/core'; +import type * as BabelTypes from '@babel/types'; +import { promises as fs } from 'fs'; +import path from 'path'; +import { signatures } from '../'; + +function applyAsyncRewriterChanges() { + return ({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + types: t, + }: { + types: typeof BabelTypes; + }): babel.PluginObj<{ + processedMethods: [string, string, BabelTypes.ClassBody][]; + }> => { + return { + pre() { + this.processedMethods = []; + }, + post() { + for (const className of Object.keys(signatures)) { + for (const methodName of Object.keys( + signatures[className].attributes ?? {} + )) { + if ( + signatures[className].attributes?.[methodName].returnsPromise && + !signatures[className].attributes?.[methodName].inherited + ) { + if ( + !this.processedMethods.find( + ([cls, method]) => cls === className && method === methodName + ) + ) { + // eslint-disable-next-line no-console + console.error( + `Expected to find and transpile type for @returnsPromise-annotated method ${className}.${methodName}` + ); + } + } + } + } + }, + visitor: { + TSDeclareMethod(path) { + if ('isMongoshAsyncRewrittenMethod' in path.node) return; + + if (path.parent.type !== 'ClassBody') return; + if (path.parentPath.parent.type !== 'ClassDeclaration') return; + const classId = path.parentPath.parent.id; + if (classId?.type !== 'Identifier') return; + const className = classId.name; + if (path.node.key.type !== 'Identifier') return; + const methodName = path.node.key.name; + + if ( + this.processedMethods.find( + ([cls, method, classBody]) => + cls === className && + method === methodName && + classBody !== path.parent + ) + ) { + throw new Error(`Duplicate method: ${className}.${methodName}`); + } + this.processedMethods.push([className, methodName, path.parent]); + + if (!signatures[className]?.attributes?.[methodName]?.returnsPromise) + return; + + const { returnType } = path.node; + if (returnType?.type !== 'TSTypeAnnotation') return; + if (returnType.typeAnnotation.type !== 'TSTypeReference') return; + if (returnType.typeAnnotation.typeName.type !== 'Identifier') return; + if (returnType.typeAnnotation.typeName.name !== 'Promise') return; + if (!returnType.typeAnnotation.typeParameters?.params.length) return; + path.replaceWith({ + ...path.node, + returnType: { + ...returnType, + typeAnnotation: + returnType.typeAnnotation.typeParameters.params[0], + }, + isMongoshAsyncRewrittenMethod: true, + }); + }, + }, + }; + }; +} + +async function main() { + const apiRaw = await fs.readFile( + path.resolve(__dirname, '..', 'lib', 'api-raw.d.ts'), + 'utf8' + ); + const result = babel.transformSync(apiRaw, { + code: true, + ast: false, + configFile: false, + babelrc: false, + browserslistConfigFile: false, + compact: false, + sourceType: 'module', + plugins: [applyAsyncRewriterChanges()], + parserOpts: { + plugins: ['typescript'], + }, + }); + const code = result?.code ?? ''; + /* + code += ` +// REPLACEME +type MongodbServerSchema = { + admin: {}, + config: {}, + test: { + test: { + schema: { + _id: ObjectId; + foo: number; + } + }, + with: { schema: never }, + 'with.dots': { + schema: { + _id: ObjectId; + bar: string; + } + } + } +} +// REPLACEME + +declare global { + // second argument optional + var db: Database; + var rs: ReplicaSet; + var sh: Shard; + // not sure this is correct because sp is usually made using static method + // Streams.newInstance(), but here Streams is only a type + var sp: Streams; + + var use: (collection: StringKey) => void; +} +`; + */ + await fs.writeFile( + path.resolve(__dirname, '..', 'lib', 'api-processed.d.ts'), + code + ); +} + +main().catch((err) => + process.nextTick(() => { + throw err; + }) +); diff --git a/packages/shell-api/package.json b/packages/shell-api/package.json index 36493be1a3..ae7ec8596b 100644 --- a/packages/shell-api/package.json +++ b/packages/shell-api/package.json @@ -4,6 +4,15 @@ "description": "MongoDB Shell API Classes Package", "main": "lib/index.js", "types": "lib/index.d.ts", + "exports": { + ".": { + "default": "./lib/index.js", + "types": "./lib/index.d.ts" + }, + "./api": { + "types": "./lib/api-processed.d.ts" + } + }, "config": { "unsafe-perm": true }, @@ -13,7 +22,8 @@ }, "scripts": { "compile": "tsc -p tsconfig.json", - "pretest": "npm run compile", + "api-generate": "api-extractor run ; ts-node bin/api-postprocess.ts", + "pretest": "npm run compile && npm run api-generate", "eslint": "eslint", "lint": "npm run eslint . && npm run prettier -- --check .", "check": "npm run lint && npm run depcheck", @@ -25,7 +35,7 @@ "test-coverage": "nyc --no-clean --cwd ../.. --reporter=none npm run test", "test-ci-coverage": "nyc --no-clean --cwd ../.. --reporter=none npm run test-ci", "test-apistrict-ci": "cross-env MONGOSH_TEST_FORCE_API_STRICT=1 npm run test-ci", - "prepublish": "npm run compile", + "prepublish": "npm run compile && npm run api-generate", "prettier": "prettier", "reformat": "npm run prettier -- --write . && npm run eslint --fix" }, @@ -40,6 +50,8 @@ "build" ], "dependencies": { + "@babel/core": "^7.26.10", + "@babel/types": "^7.26.10", "@mongosh/arg-parser": "3.10.0", "@mongosh/errors": "2.4.0", "@mongosh/history": "2.4.6", @@ -48,6 +60,7 @@ "mongodb-redact": "^1.1.5" }, "devDependencies": { + "@microsoft/api-extractor": "^7.39.3", "@mongodb-js/eslint-config-mongosh": "^1.0.0", "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-mongosh": "^1.0.0", diff --git a/packages/shell-api/src/aggregation-cursor.spec.ts b/packages/shell-api/src/aggregation-cursor.spec.ts index 7784f37478..cf470b8694 100644 --- a/packages/shell-api/src/aggregation-cursor.spec.ts +++ b/packages/shell-api/src/aggregation-cursor.spec.ts @@ -36,6 +36,7 @@ describe('AggregationCursor', function () { type: 'function', returnsPromise: false, deprecated: false, + inherited: true, returnType: 'AggregationCursor', platforms: ALL_PLATFORMS, topologies: ALL_TOPOLOGIES, diff --git a/packages/shell-api/src/api.ts b/packages/shell-api/src/api.ts new file mode 100644 index 0000000000..e1300edfca --- /dev/null +++ b/packages/shell-api/src/api.ts @@ -0,0 +1,102 @@ +// TODO: does it make sense to have all this stuff? Don't we just need enough for the top-level API, ie. the globals? +import AggregationCursor from './aggregation-cursor'; +import RunCommandCursor from './run-command-cursor'; +export { Collection } from './collection'; +import Cursor from './cursor'; +import Database, { CollectionNamesWithTypes } from './database'; +import Explainable from './explainable'; +import ExplainableCursor from './explainable-cursor'; +import Help, { HelpProperties } from './help'; +export { + ShellInstanceState, + EvaluationListener, + ShellCliOptions, + OnLoadResult, + ShellPlugin, + AutocompleteParameters, +} from './shell-instance-state'; +export type { ShellBson } from './shell-bson'; +import Shard from './shard'; +import ReplicaSet from './replica-set'; +import ShellApi from './shell-api'; +export { + BulkWriteResult, + CommandResult, + CursorIterationResult, + DeleteResult, + InsertManyResult, + InsertOneResult, + UpdateResult, +} from './result'; +import Mongo from './mongo'; +export { + signatures, + ShellResult, + toShellResult, + getShellApiType, + TypeSignature, + Namespace, +} from './decorators'; +import { Topologies, ServerVersions } from './enums'; +import { InterruptFlag } from './interruptor'; +export type { + GenericCollectionSchema, + GenericDatabaseSchema, + GenericServerSideSchema, + StringKey, + FindAndModifyMethodShellOptions, + FindAndModifyShellOptions, + RemoveShellOptions, +} from './helpers'; +export type { Streams } from './streams'; +export type { StreamProcessor } from './stream-processor'; + +export { + AggregationCursor, + RunCommandCursor, + CollectionNamesWithTypes, + Cursor, + Database, + Explainable, + ExplainableCursor, + Help, + HelpProperties, + Mongo, + Shard, + ReplicaSet, + ShellApi, + ServerVersions, + Topologies, + InterruptFlag, +}; + +// TODO: do we really want all these? + +/* +export { AggregateOrFindCursor } from './aggregate-or-find-cursor'; +export { + ServiceProviderFindCursor, + ServiceProviderAggregationCursor, + ReadPreferenceLike, + ReadConcernLevel, + TagSet, + CollationOptions, + HedgeOptions, + ExplainVerbosityLike, + FindOptions, + CountOptions, + DistinctOptions, + FindOneAndDeleteOptions, + FindOneAndReplaceOptions, + FindOneAndUpdateOptions, + UpdateOptions, + CommandOperationOptions, + AggregateOptions +} from '@mongosh/service-provider-core'; +export { DatabaseImpl } from './database'; +export { CollectionImpl } from './collection'; +export { ShellResultSourceInformation, ShellCommandCompleter, Signatures, ShellApiWithMongoClass } from './decorators'; +export { MapReduceShellOptions } from './helpers'; + +// and many more +*/ diff --git a/packages/shell-api/src/bulk.spec.ts b/packages/shell-api/src/bulk.spec.ts index 54947ead9f..58b06bbb7a 100644 --- a/packages/shell-api/src/bulk.spec.ts +++ b/packages/shell-api/src/bulk.spec.ts @@ -11,7 +11,7 @@ import type { EventEmitter } from 'events'; import type { StubbedInstance } from 'ts-sinon'; import { stubInterface } from 'ts-sinon'; import Bulk, { BulkFindOp } from './bulk'; -import Collection from './collection'; +import { CollectionImpl } from './collection'; import { ALL_PLATFORMS, ALL_SERVER_VERSIONS, ALL_TOPOLOGIES } from './enums'; import { signatures, toShellResult } from './index'; import { BulkWriteResult } from './result'; @@ -57,7 +57,7 @@ describe('Bulk API', function () { }); describe('Metadata', function () { describe('toShellResult', function () { - const collection = stubInterface(); + const collection = stubInterface(); const b = new Bulk(collection, { batches: [1, 2, 3, 4], } as any); @@ -77,7 +77,7 @@ describe('Bulk API', function () { ['ordered', 'unordered'].forEach((t) => { describe(t, function () { describe('commands', function () { - let collection: Collection; + let collection: CollectionImpl; let serviceProvider: StubbedInstance; let bulk: Bulk; let bus: StubbedInstance; @@ -101,7 +101,7 @@ describe('Bulk API', function () { serviceProvider.runCommand.resolves({ ok: 1 }); instanceState = new ShellInstanceState(serviceProvider, bus); const db = instanceState.currentDb; - collection = new Collection(db._mongo, db, 'coll1'); + collection = new CollectionImpl(db._mongo, db, 'coll1'); innerStub = stubInterface(); innerStub.batches = [ { originalZeroIndex: 0 }, diff --git a/packages/shell-api/src/collection.spec.ts b/packages/shell-api/src/collection.spec.ts index 63b7480072..60cd53d18d 100644 --- a/packages/shell-api/src/collection.spec.ts +++ b/packages/shell-api/src/collection.spec.ts @@ -11,9 +11,9 @@ import { shellApiType, ADMIN_DB, } from './enums'; -import Database from './database'; +import { DatabaseImpl } from './database'; import Mongo from './mongo'; -import Collection from './collection'; +import { CollectionImpl } from './collection'; import ChangeStreamCursor from './change-stream-cursor'; import Explainable from './explainable'; import type { @@ -33,13 +33,14 @@ import { MongoshInvalidInputError, MongoshRuntimeError, } from '@mongosh/errors'; +import type { StringKey } from './helpers'; const sinonChai = require('sinon-chai'); // weird with import use(sinonChai); describe('Collection', function () { describe('help', function () { - const apiClass = new Collection({} as any, {} as any, 'name'); + const apiClass = new CollectionImpl({} as any, {} as any, 'name'); it('calls help function', async function () { expect((await toShellResult(apiClass.help())).type).to.equal('Help'); expect((await toShellResult(apiClass.help)).type).to.equal('Help'); @@ -47,10 +48,11 @@ describe('Collection', function () { }); describe('signatures', function () { it('type', function () { - expect(signatures.Collection.type).to.equal('Collection'); + // TODO: do we want the signatures to be CollectionImpl or Collection? + expect(signatures.CollectionImpl.type).to.equal('CollectionImpl'); }); it('attributes', function () { - expect(signatures.Collection.attributes?.aggregate).to.deep.equal({ + expect(signatures.CollectionImpl.attributes?.aggregate).to.deep.equal({ type: 'function', returnsPromise: true, deprecated: false, @@ -68,10 +70,14 @@ describe('Collection', function () { describe('metadata', function () { describe('toShellResult', function () { const mongo = sinon.spy(); - const db = new Database(mongo as any, 'myDB'); - const coll = new Collection(mongo as any, db, 'myCollection'); + const db = new DatabaseImpl(mongo as any, 'myDB'); + const coll = new CollectionImpl( + mongo as any, + db._typeLaunder(), + 'myCollection' + ); it('toShellResult', async function () { - expect((await toShellResult(coll)).type).to.equal('Collection'); + expect((await toShellResult(coll)).type).to.equal('CollectionImpl'); expect((await toShellResult(coll)).printable).to.equal( 'myDB.myCollection' ); @@ -80,21 +86,25 @@ describe('Collection', function () { }); describe('.collections', function () { it('allows to get a collection as property if is not one of the existing methods', function () { - const database = new Database( + const database = new DatabaseImpl( { _instanceState: { emitApiCallWithArgs: (): void => {} } } as any, 'db1' ); - const coll: any = new Collection({} as any, database, 'coll'); - expect(coll.someCollection).to.have.instanceOf(Collection); + const coll: any = new CollectionImpl( + {} as any, + database._typeLaunder(), + 'coll' + ); + expect(coll.someCollection).to.have.instanceOf(CollectionImpl); expect(coll.someCollection._name).to.equal('coll.someCollection'); }); it('reuses collections', function () { - const database: any = new Database( + const database: any = new DatabaseImpl( { _instanceState: { emitApiCallWithArgs: (): void => {} } } as any, 'db1' ); - const coll: any = new Collection({} as any, database, 'coll'); + const coll: any = new CollectionImpl({} as any, database, 'coll'); expect(coll.someCollection).to.equal( database.getCollection('coll.someCollection') ); @@ -103,36 +113,47 @@ describe('Collection', function () { it('does not return a collection starting with _', function () { // this is the behaviour in the old shell - const database: any = new Database({} as any, 'db1'); - const coll: any = new Collection({} as any, database, 'coll'); + const database: any = new DatabaseImpl({} as any, 'db1'); + const coll: any = new CollectionImpl({} as any, database, 'coll'); expect(coll._someProperty).to.equal(undefined); }); it('does not return a collection for symbols', function () { - const database: any = new Database({} as any, 'db1'); - const coll: any = new Collection({} as any, database, 'coll'); + const database: any = new DatabaseImpl({} as any, 'db1'); + const coll: any = new CollectionImpl({} as any, database, 'coll'); expect(coll[Symbol('someProperty')]).to.equal(undefined); }); it('does not return a collection with invalid name', function () { - const database: any = new Database({} as any, 'db1'); - const coll: any = new Collection({} as any, database, 'coll'); + const database: any = new DatabaseImpl({} as any, 'db1'); + const coll: any = new CollectionImpl({} as any, database, 'coll'); expect(coll.foo$bar).to.equal(undefined); }); it('allows to access _name', function () { - const database: any = new Database({} as any, 'db1'); - const coll: any = new Collection({} as any, database, 'coll'); + const database: any = new DatabaseImpl({} as any, 'db1'); + const coll: any = new CollectionImpl({} as any, database, 'coll'); expect(coll._name).to.equal('coll'); }); }); describe('commands', function () { - let mongo: Mongo; + type ServerSchema = { + db1: { + coll1: { + schema: {}; + }; + }; + }; + let mongo: Mongo; let serviceProvider: StubbedInstance; - let database: Database; + let database: DatabaseImpl; let bus: StubbedInstance; let instanceState: ShellInstanceState; - let collection: Collection; + let collection: CollectionImpl< + ServerSchema, + ServerSchema['db1'], + ServerSchema['db1']['coll1'] + >; beforeEach(function () { bus = stubInterface(); @@ -149,8 +170,15 @@ describe('Collection', function () { undefined, serviceProvider ); - database = new Database(mongo, 'db1'); - collection = new Collection(mongo, database, 'coll1'); + database = new DatabaseImpl( + mongo, + 'db1' as StringKey + ); + collection = new CollectionImpl< + ServerSchema, + ServerSchema['db1'], + ServerSchema['db1']['coll1'] + >(mongo, database._typeLaunder(), 'coll1'); }); describe('aggregate', function () { let serviceProviderCursor: StubbedInstance; @@ -2802,13 +2830,24 @@ describe('Collection', function () { }); describe('fle2', function () { - let mongo1: Mongo; - let mongo2: Mongo; + type ServerSchema = { + db1: { + collfle2: { + schema: {}; + }; + }; + }; + let mongo1: Mongo; + let mongo2: Mongo; let serviceProvider: StubbedInstance; - let database: Database; + let database: DatabaseImpl; let bus: StubbedInstance; let instanceState: ShellInstanceState; - let collection: Collection; + let collection: CollectionImpl< + ServerSchema, + ServerSchema['db1'], + ServerSchema['db1']['collfle2'] + >; let keyId: any[]; beforeEach(function () { bus = stubInterface(); @@ -2821,7 +2860,8 @@ describe('Collection', function () { keyId = [ { $binary: { base64: 'oh3caogGQ4Sf34ugKnZ7Xw==', subType: '04' } }, ]; - mongo1 = new Mongo( + + mongo1 = new Mongo( instanceState, undefined, { @@ -2836,8 +2876,15 @@ describe('Collection', function () { undefined, serviceProvider ); - database = new Database(mongo1, 'db1'); - collection = new Collection(mongo1, database, 'collfle2'); + database = new DatabaseImpl( + mongo1, + 'db1' as StringKey + ); + collection = new CollectionImpl( + mongo1, + database._typeLaunder(), + 'collfle2' as StringKey + ); mongo2 = new Mongo( instanceState, undefined, @@ -2896,10 +2943,10 @@ describe('Collection', function () { }); describe('with session', function () { let serviceProvider: StubbedInstance; - let collection: Collection; + let collection: CollectionImpl; let internalSession: StubbedInstance; const exceptions: { - [key in keyof (typeof Collection)['prototype']]?: { + [key in keyof (typeof CollectionImpl)['prototype']]?: { a?: any; m?: string; i?: number; @@ -2961,7 +3008,7 @@ describe('Collection', function () { getSearchIndexes: { i: 3 }, checkMetadataConsistency: { m: 'runCursorCommand', i: 2 }, }; - const ignore: (keyof (typeof Collection)['prototype'])[] = [ + const ignore: (keyof (typeof CollectionImpl)['prototype'])[] = [ 'getShardDistribution', 'stats', 'isCapped', @@ -3034,8 +3081,8 @@ describe('Collection', function () { }); context('all commands that use the same command in sp', function () { for (const method of ( - Object.getOwnPropertyNames(Collection.prototype) as (string & - keyof (typeof Collection)['prototype'])[] + Object.getOwnPropertyNames(CollectionImpl.prototype) as (string & + keyof (typeof CollectionImpl)['prototype'])[] ).filter( (k) => typeof k === 'string' && @@ -3045,7 +3092,7 @@ describe('Collection', function () { if ( !method.startsWith('_') && !method.startsWith('print') && - Collection.prototype[method].returnsPromise + CollectionImpl.prototype[method].returnsPromise ) { it(`passes the session through for ${method}`, async function () { try { diff --git a/packages/shell-api/src/collection.ts b/packages/shell-api/src/collection.ts index 7b7432d77b..535c72f740 100644 --- a/packages/shell-api/src/collection.ts +++ b/packages/shell-api/src/collection.ts @@ -22,6 +22,10 @@ import type { FindAndModifyMethodShellOptions, RemoveShellOptions, MapReduceShellOptions, + GenericCollectionSchema, + GenericDatabaseSchema, + GenericServerSideSchema, + StringKey, SearchIndexDefinition, } from './helpers'; import { @@ -43,7 +47,6 @@ import { buildConfigChunksCollectionMatch, onlyShardedCollectionsInConfigFilter, aggregateBackgroundOptionNotSupportedHelp, - getConfigDB, } from './helpers'; import type { AnyBulkWriteOperation, @@ -94,13 +97,37 @@ import PlanCache from './plan-cache'; import ChangeStreamCursor from './change-stream-cursor'; import { ShellApiErrors } from './error-codes'; +export type Collection< + M extends GenericServerSideSchema = GenericServerSideSchema, + D extends GenericDatabaseSchema = M[keyof M], + C extends GenericCollectionSchema = D[keyof D], + N extends StringKey = StringKey +> = CollectionImpl & { + [k in StringKey as k extends `${N}.${infer S}` ? S : never]: Collection< + M, + D, + D[k], + k + >; +}; + @shellApiClassDefault @addSourceToResults -export default class Collection extends ShellApiWithMongoClass { - _mongo: Mongo; - _database: Database; - _name: string; - constructor(mongo: Mongo, database: Database, name: string) { +export class CollectionImpl< + M extends GenericServerSideSchema = GenericServerSideSchema, + D extends GenericDatabaseSchema = M[keyof M], + C extends GenericCollectionSchema = D[keyof D], + N extends StringKey = StringKey +> extends ShellApiWithMongoClass { + _mongo: Mongo; + _database: Database; + _name: N; + + _typeLaunder(): Collection { + return this as Collection; + } + + constructor(mongo: Mongo, database: Database, name: N) { super(); this._mongo = mongo; this._database = database; @@ -536,7 +563,7 @@ export default class Collection extends ShellApiWithMongoClass { query: Document = {}, projection?: Document, options: FindOptions = {} - ): Promise { + ): Promise { if (projection) { options.projection = projection; } @@ -1420,7 +1447,7 @@ export default class Collection extends ShellApiWithMongoClass { * @return {Database} */ @returnType('Database') - getDB(): Database { + getDB(): Database { this._emitCollectionApiCall('getDB'); return this._database; } @@ -1431,7 +1458,7 @@ export default class Collection extends ShellApiWithMongoClass { * @return {Mongo} */ @returnType('Mongo') - getMongo(): Mongo { + getMongo(): Mongo { this._emitCollectionApiCall('getMongo'); return this._mongo; } @@ -1561,9 +1588,9 @@ export default class Collection extends ShellApiWithMongoClass { return `${this._database._name}.${this._name}`; } - getName(): string { + getName(): N { this._emitCollectionApiCall('getName'); - return `${this._name}`; + return this._name; } @returnsPromise @@ -1610,7 +1637,7 @@ export default class Collection extends ShellApiWithMongoClass { explain(verbosity: ExplainVerbosityLike = 'queryPlanner'): Explainable { verbosity = validateExplainableVerbosity(verbosity); this._emitCollectionApiCall('explain', { verbosity }); - return new Explainable(this._mongo, this, verbosity); + return new Explainable(this._mongo, this._typeLaunder(), verbosity); } /** @@ -1768,7 +1795,7 @@ export default class Collection extends ShellApiWithMongoClass { } const ns = `${this._database._name}.${this._name}`; - const config = this._mongo.getDB('config'); + const config = this._mongo.getDB('config' as StringKey); if (collStats[0].shard) { result.shards = shardStats; } @@ -1981,7 +2008,7 @@ export default class Collection extends ShellApiWithMongoClass { true, await this._database._baseOptions() ); - return new Bulk(this, innerBulk, true); + return new Bulk(this._typeLaunder(), innerBulk, true); } @returnsPromise @@ -1995,14 +2022,14 @@ export default class Collection extends ShellApiWithMongoClass { false, await this._database._baseOptions() ); - return new Bulk(this, innerBulk); + return new Bulk(this._typeLaunder(), innerBulk); } @returnType('PlanCache') @apiVersions([]) getPlanCache(): PlanCache { this._emitCollectionApiCall('getPlanCache'); - return new PlanCache(this); + return new PlanCache(this._typeLaunder()); } @returnsPromise @@ -2135,10 +2162,8 @@ export default class Collection extends ShellApiWithMongoClass { > { this._emitCollectionApiCall('getShardDistribution', {}); - await getConfigDB(this._database); // Warns if not connected to mongos - - const result = {} as GetShardDistributionResult; - const config = this._mongo.getDB('config'); + const result = {} as Document; + const config = this._mongo.getDB('config' as StringKey); const collStats = await ( await this.aggregate({ $collStats: { storageStats: {} } }) @@ -2248,7 +2273,10 @@ export default class Collection extends ShellApiWithMongoClass { } result.Totals = totalValue; - return new CommandResult('StatsResult', result); + return new CommandResult( + 'StatsResult', + result as GetShardDistributionResult + ); } @serverVersions(['3.1.0', ServerVersions.latest]) @@ -2310,7 +2338,7 @@ export default class Collection extends ShellApiWithMongoClass { @apiVersions([1]) async hideIndex(index: string | Document): Promise { this._emitCollectionApiCall('hideIndex'); - return setHideIndex(this, index, true); + return setHideIndex(this._typeLaunder(), index, true); } @serverVersions(['4.4.0', ServerVersions.latest]) @@ -2318,7 +2346,7 @@ export default class Collection extends ShellApiWithMongoClass { @apiVersions([1]) async unhideIndex(index: string | Document): Promise { this._emitCollectionApiCall('unhideIndex'); - return setHideIndex(this, index, false); + return setHideIndex(this._typeLaunder(), index, false); } @serverVersions(['7.0.0', ServerVersions.latest]) @@ -2519,3 +2547,5 @@ export type GetShardDistributionResult = { 'estimated docs per chunk': number; }; }; + +export default Collection; diff --git a/packages/shell-api/src/cursor.spec.ts b/packages/shell-api/src/cursor.spec.ts index 3ec90db013..72c639d5be 100644 --- a/packages/shell-api/src/cursor.spec.ts +++ b/packages/shell-api/src/cursor.spec.ts @@ -55,6 +55,7 @@ describe('Cursor', function () { type: 'function', returnsPromise: false, deprecated: false, + inherited: true, returnType: 'Cursor', platforms: ALL_PLATFORMS, topologies: ALL_TOPOLOGIES, diff --git a/packages/shell-api/src/database.spec.ts b/packages/shell-api/src/database.spec.ts index 4cc7cb7e22..531948c60f 100644 --- a/packages/shell-api/src/database.spec.ts +++ b/packages/shell-api/src/database.spec.ts @@ -6,8 +6,8 @@ import { stubInterface } from 'ts-sinon'; import type { EventEmitter } from 'events'; import { ALL_PLATFORMS, ALL_SERVER_VERSIONS, ALL_TOPOLOGIES } from './enums'; import { signatures, toShellResult } from './index'; -import Database from './database'; -import Collection from './collection'; +import { DatabaseImpl } from './database'; +import { CollectionImpl } from './collection'; import Mongo from './mongo'; import type { AggregationCursor as ServiceProviderAggCursor, @@ -41,7 +41,7 @@ describe('Database', function () { .update('anna:mongo:pwd') .digest('hex'); describe('help', function () { - const apiClass: any = new Database({} as any, 'name'); + const apiClass: any = new DatabaseImpl({} as any, 'name'); it('calls help function', async function () { expect((await toShellResult(apiClass.help())).type).to.equal('Help'); expect((await toShellResult(apiClass.help)).type).to.equal('Help'); @@ -57,49 +57,49 @@ describe('Database', function () { }); describe('collections', function () { it('allows to get a collection as property if is not one of the existing methods', function () { - const database: any = new Database({} as any, 'db1'); - expect(database.someCollection).to.have.instanceOf(Collection); + const database: any = new DatabaseImpl({} as any, 'db1'); + expect(database.someCollection).to.have.instanceOf(CollectionImpl); expect(database.someCollection._name).to.equal('someCollection'); }); it('reuses collections', function () { - const database: any = new Database({} as any, 'db1'); + const database: any = new DatabaseImpl({} as any, 'db1'); expect(database.someCollection).to.equal(database.someCollection); }); it('does not return a collection starting with _', function () { // this is the behaviour in the old shell - const database: any = new Database({} as any, 'db1'); + const database: any = new DatabaseImpl({} as any, 'db1'); expect(database._someProperty).to.equal(undefined); }); it('does not return a collection for symbols', function () { - const database: any = new Database({} as any, 'db1'); + const database: any = new DatabaseImpl({} as any, 'db1'); expect(database[Symbol('someProperty')]).to.equal(undefined); }); it('does not return a collection with invalid name', function () { - const database: any = new Database({} as any, 'db1'); + const database: any = new DatabaseImpl({} as any, 'db1'); expect(database.foo$bar).to.equal(undefined); }); it('allows to access _name', function () { - const database: any = new Database({} as any, 'db1'); + const database: any = new DatabaseImpl({} as any, 'db1'); expect(database._name).to.equal('db1'); }); it('allows to access collections', function () { - const database: any = new Database({} as any, 'db1'); + const database: any = new DatabaseImpl({} as any, 'db1'); expect(database._collections).to.deep.equal({}); }); }); describe('signatures', function () { it('type', function () { - expect(signatures.Database.type).to.equal('Database'); + expect(signatures.DatabaseImpl.type).to.equal('DatabaseImpl'); }); it('attributes', function () { - expect(signatures.Database.attributes?.aggregate).to.deep.equal({ + expect(signatures.DatabaseImpl.attributes?.aggregate).to.deep.equal({ type: 'function', returnsPromise: true, deprecated: false, @@ -117,26 +117,26 @@ describe('Database', function () { describe('Metadata', function () { describe('toShellResult', function () { const mongo = sinon.spy(); - const db = new Database(mongo as any, 'myDB'); + const db = new DatabaseImpl(mongo as any, 'myDB'); it('value', async function () { expect((await toShellResult(db)).printable).to.equal('myDB'); }); it('type', async function () { - expect((await toShellResult(db)).type).to.equal('Database'); + expect((await toShellResult(db)).type).to.equal('DatabaseImpl'); }); }); }); describe('attributes', function () { const mongo = sinon.spy(); - const db = new Database(mongo as any, 'myDB') as any; + const db = new DatabaseImpl(mongo as any, 'myDB') as any; it('creates new collection for attribute', async function () { - expect((await toShellResult(db.coll)).type).to.equal('Collection'); + expect((await toShellResult(db.coll)).type).to.equal('CollectionImpl'); }); }); describe('commands', function () { let mongo: Mongo; let serviceProvider: StubbedInstance; - let database: Database; + let database: DatabaseImpl; let bus: StubbedInstance; let instanceState: ShellInstanceState; @@ -155,7 +155,7 @@ describe('Database', function () { undefined, serviceProvider ); - database = new Database(mongo, 'db1'); + database = new DatabaseImpl(mongo, 'db1'); }); describe('getCollectionInfos', function () { it('returns the result of serviceProvider.listCollections', async function () { @@ -494,7 +494,7 @@ describe('Database', function () { describe('getSiblingDB', function () { it('returns a database', function () { const otherDb = database.getSiblingDB('otherdb'); - expect(otherDb).to.be.instanceOf(Database); + expect(otherDb).to.be.instanceOf(DatabaseImpl); expect(otherDb._name).to.equal('otherdb'); }); @@ -525,7 +525,7 @@ describe('Database', function () { describe('getCollection', function () { it('returns a collection for the database', function () { const coll = database.getCollection('coll'); - expect(coll).to.be.instanceOf(Collection); + expect(coll).to.be.instanceOf(CollectionImpl); expect(coll._name).to.equal('coll'); expect(coll._database).to.equal(database); }); @@ -533,12 +533,12 @@ describe('Database', function () { it('returns a collection for Object.prototype keys', function () { { const coll = database.getCollection('__proto__'); - expect(coll).to.be.instanceOf(Collection); + expect(coll).to.be.instanceOf(CollectionImpl); expect(coll._name).to.equal('__proto__'); } { const coll = database.getCollection('hasOwnProperty'); - expect(coll).to.be.instanceOf(Collection); + expect(coll).to.be.instanceOf(CollectionImpl); expect(coll._name).to.equal('hasOwnProperty'); } }); @@ -584,13 +584,13 @@ describe('Database', function () { it('allows to use collection names that would collide with methods', function () { const coll = database.getCollection('getCollection'); - expect(coll).to.be.instanceOf(Collection); + expect(coll).to.be.instanceOf(CollectionImpl); expect(coll._name).to.equal('getCollection'); }); it('allows to use collection names that starts with _', function () { const coll = database.getCollection('_coll1'); - expect(coll).to.be.instanceOf(Collection); + expect(coll).to.be.instanceOf(CollectionImpl); expect(coll._name).to.equal('_coll1'); }); @@ -776,7 +776,7 @@ describe('Database', function () { context('on $external database', function () { beforeEach(function () { - database = new Database(mongo, '$external'); + database = new DatabaseImpl(mongo, '$external'); }); it('can create a user without password', async function () { @@ -2986,7 +2986,7 @@ describe('Database', function () { }); describe('with session', function () { let serviceProvider: StubbedInstance; - let database: Database; + let database: DatabaseImpl; let internalSession: StubbedInstance; const exceptions = { getCollectionNames: { m: 'listCollections' }, @@ -3064,8 +3064,8 @@ describe('Database', function () { }); it('all commands that use runCommandWithCheck', async function () { for (const method of ( - Object.getOwnPropertyNames(Database.prototype) as (string & - keyof Database)[] + Object.getOwnPropertyNames(DatabaseImpl.prototype) as (string & + keyof DatabaseImpl)[] ).filter( (k) => typeof k === 'string' && diff --git a/packages/shell-api/src/database.ts b/packages/shell-api/src/database.ts index c148cc14e1..cd96975306 100644 --- a/packages/shell-api/src/database.ts +++ b/packages/shell-api/src/database.ts @@ -1,5 +1,6 @@ import type Mongo from './mongo'; -import Collection from './collection'; +import type Collection from './collection'; +import { CollectionImpl } from './collection'; import { returnsPromise, returnType, @@ -11,6 +12,11 @@ import { ShellApiWithMongoClass, } from './decorators'; import { asPrintable, ServerVersions, Topologies } from './enums'; +import type { + GenericDatabaseSchema, + GenericServerSideSchema, + StringKey, +} from './helpers'; import { adaptAggregateOptions, adaptOptions, @@ -68,23 +74,37 @@ type AuthDoc = { mechanism?: string; }; +export type Database< + M extends GenericServerSideSchema = GenericServerSideSchema, + D extends GenericDatabaseSchema = GenericDatabaseSchema +> = DatabaseImpl & { + [k in StringKey]: Collection; +}; + @shellApiClassDefault -export default class Database extends ShellApiWithMongoClass { - _mongo: Mongo; - _name: string; - _collections: Record; +export class DatabaseImpl< + M extends GenericServerSideSchema = GenericServerSideSchema, + D extends GenericDatabaseSchema = GenericDatabaseSchema +> extends ShellApiWithMongoClass { + _mongo: Mongo; + _name: StringKey; + _collections: Record>; _session: Session | undefined; - _cachedCollectionNames: string[] = []; + _cachedCollectionNames: StringKey[] = []; _cachedHello: Document | null = null; - constructor(mongo: Mongo, name: string, session?: Session) { + _typeLaunder(): Database { + return this as Database; + } + + constructor(mongo: Mongo, name: StringKey, session?: Session) { super(); this._mongo = mongo; this._name = name; - const collections: Record = Object.create(null); + const collections: Record> = Object.create(null); this._collections = collections; this._session = session; - const proxy = new Proxy(this, { + const proxy = new Proxy(this._typeLaunder(), { get: (target, prop): any => { if (prop in target) { return (target as any)[prop]; @@ -99,7 +119,11 @@ export default class Database extends ShellApiWithMongoClass { } if (!collections[prop]) { - collections[prop] = new Collection(mongo, proxy, prop); + collections[prop] = new CollectionImpl( + mongo, + proxy, + prop + )._typeLaunder(); } return collections[prop]; @@ -315,11 +339,11 @@ export default class Database extends ShellApiWithMongoClass { } @returnType('Mongo') - getMongo(): Mongo { + getMongo(): Mongo { return this._mongo; } - getName(): string { + getName(): StringKey { return this._name; } @@ -330,9 +354,9 @@ export default class Database extends ShellApiWithMongoClass { */ @returnsPromise @apiVersions([1]) - async getCollectionNames(): Promise { + async getCollectionNames(): Promise[]> { this._emitDatabaseApiCall('getCollectionNames'); - return this._getCollectionNames(); + return (await this._getCollectionNames()) as StringKey[]; } /** @@ -473,17 +497,17 @@ export default class Database extends ShellApiWithMongoClass { } @returnType('Database') - getSiblingDB(db: string): Database { + getSiblingDB>(db: K): Database { assertArgsDefinedType([db], ['string'], 'Database.getSiblingDB'); this._emitDatabaseApiCall('getSiblingDB', { db }); if (this._session) { - return this._session.getDatabase(db); + return this._session.getDatabase(db) as Database; } return this._mongo._getDb(db); } @returnType('Collection') - getCollection(coll: string): Collection { + getCollection>(coll: K): Collection { assertArgsDefinedType([coll], ['string'], 'Database.getColl'); this._emitDatabaseApiCall('getCollection', { coll }); if (!isValidCollectionName(coll)) { @@ -493,13 +517,17 @@ export default class Database extends ShellApiWithMongoClass { ); } - const collections: Record = this._collections; + const collections: Record> = this._collections; if (!collections[coll]) { - collections[coll] = new Collection(this._mongo, this, coll); + collections[coll] = new CollectionImpl( + this._mongo, + this._typeLaunder(), + coll + )._typeLaunder(); } - return collections[coll]; + return collections[coll] as Collection; } @returnsPromise @@ -1505,7 +1533,7 @@ export default class Database extends ShellApiWithMongoClass { async printShardingStatus(verbose = false): Promise { this._emitDatabaseApiCall('printShardingStatus', { verbose }); const result = await getPrintableShardStatus( - await getConfigDB(this), + await getConfigDB(this._typeLaunder()), verbose ); return new CommandResult('StatsResult', result); @@ -1797,3 +1825,5 @@ export default class Database extends ShellApiWithMongoClass { }); } } + +export default Database; diff --git a/packages/shell-api/src/decorators.ts b/packages/shell-api/src/decorators.ts index a8839bdfcc..9c6ca8b380 100644 --- a/packages/shell-api/src/decorators.ts +++ b/packages/shell-api/src/decorators.ts @@ -381,6 +381,7 @@ export interface TypeSignature { isDirectShellCommand?: boolean; acceptsRawInput?: boolean; shellCommandCompleter?: ShellCommandCompleter; + inherited?: boolean; } /** @@ -426,6 +427,7 @@ type ClassSignature = { isDirectShellCommand: boolean; acceptsRawInput?: boolean; shellCommandCompleter?: ShellCommandCompleter; + inherited?: true; }; }; }; @@ -582,6 +584,7 @@ function shellApiClassGeneric( isDirectShellCommand: method.isDirectShellCommand, acceptsRawInput: method.acceptsRawInput, shellCommandCompleter: method.shellCommandCompleter, + inherited: true, }; const attributeHelpKeyPrefix = `${superClassHelpKeyPrefix}.attributes.${propertyName}`; diff --git a/packages/shell-api/src/explainable-cursor.spec.ts b/packages/shell-api/src/explainable-cursor.spec.ts index 3b6c2331c5..55ee41c3f2 100644 --- a/packages/shell-api/src/explainable-cursor.spec.ts +++ b/packages/shell-api/src/explainable-cursor.spec.ts @@ -31,6 +31,7 @@ describe('ExplainableCursor', function () { type: 'function', returnsPromise: false, deprecated: false, + inherited: true, returnType: 'ExplainableCursor', platforms: ALL_PLATFORMS, topologies: ALL_TOPOLOGIES, diff --git a/packages/shell-api/src/explainable.spec.ts b/packages/shell-api/src/explainable.spec.ts index 044aebce14..9b7e68d27e 100644 --- a/packages/shell-api/src/explainable.spec.ts +++ b/packages/shell-api/src/explainable.spec.ts @@ -6,10 +6,10 @@ import type { EventEmitter } from 'events'; import { ALL_PLATFORMS, ALL_SERVER_VERSIONS, ALL_TOPOLOGIES } from './enums'; import type { ExplainableCursor } from './index'; import { signatures, toShellResult } from './index'; -import Database from './database'; +import { DatabaseImpl } from './database'; import type Cursor from './cursor'; import Mongo from './mongo'; -import Collection from './collection'; +import { CollectionImpl } from './collection'; import Explainable from './explainable'; import type { ServiceProvider, Document } from '@mongosh/service-provider-core'; import { bson } from '@mongosh/service-provider-core'; @@ -49,8 +49,8 @@ describe('Explainable', function () { }); describe('metadata', function () { const mongo: any = { _instanceState: { emitApiCallWithArgs: sinon.spy() } }; - const db = new Database(mongo, 'myDB'); - const coll = new Collection(mongo, db, 'myCollection'); + const db = new DatabaseImpl(mongo, 'myDB'); + const coll = new CollectionImpl(mongo, db._typeLaunder(), 'myCollection'); const explainable = new Explainable(mongo, coll, 'queryPlannerExtended'); it('toShellResult', async function () { const result = await toShellResult(explainable); @@ -61,10 +61,10 @@ describe('Explainable', function () { describe('commands', function () { let mongo: Mongo; let serviceProvider: StubbedInstance; - let database: Database; + let database: DatabaseImpl; let bus: StubbedInstance; let instanceState: ShellInstanceState; - let collection: Collection; + let collection: CollectionImpl; let explainable: Explainable; beforeEach(function () { @@ -80,8 +80,8 @@ describe('Explainable', function () { undefined, serviceProvider ); - database = new Database(mongo, 'db1'); - collection = new Collection(mongo, database, 'coll1'); + database = new DatabaseImpl(mongo, 'db1'); + collection = new CollectionImpl(mongo, database._typeLaunder(), 'coll1'); explainable = new Explainable(mongo, collection, 'queryPlanner'); }); describe('getCollection', function () { diff --git a/packages/shell-api/src/field-level-encryption.spec.ts b/packages/shell-api/src/field-level-encryption.spec.ts index 9fab4b6246..b506e8f5c3 100644 --- a/packages/shell-api/src/field-level-encryption.spec.ts +++ b/packages/shell-api/src/field-level-encryption.spec.ts @@ -34,7 +34,7 @@ import { makeFakeHTTPConnection, fakeAWSHandlers, } from '../../../testing/fake-kms'; -import Collection from './collection'; +import { CollectionImpl } from './collection'; import { dummyOptions } from './helpers.spec'; import type { IncomingMessage } from 'http'; @@ -602,7 +602,7 @@ describe('Field Level Encryption', function () { collName, createCollectionOptions ); - expect(collection).to.be.instanceOf(Collection); + expect(collection).to.be.instanceOf(CollectionImpl); expect(encryptedFields).to.deep.equal( libmongocResponse.encryptedFields ); diff --git a/packages/shell-api/src/helpers.spec.ts b/packages/shell-api/src/helpers.spec.ts index f39227fe02..cbaef82e86 100644 --- a/packages/shell-api/src/helpers.spec.ts +++ b/packages/shell-api/src/helpers.spec.ts @@ -8,7 +8,7 @@ import { tsToSeconds, validateExplainableVerbosity, } from './helpers'; -import { Database, Mongo, ShellInstanceState } from './index'; +import { DatabaseImpl, Mongo, ShellInstanceState } from './index'; import constructShellBson from './shell-bson'; import type { ServiceProvider } from '@mongosh/service-provider-core'; import { bson } from '@mongosh/service-provider-core'; @@ -131,7 +131,7 @@ describe('getPrintableShardStatus', function () { const testServer = startSharedTestServer(); let mongo: Mongo; - let configDatabase: Database; + let configDatabase: DatabaseImpl; let serviceProvider: ServiceProvider; let inBalancerRound = false; @@ -164,7 +164,7 @@ describe('getPrintableShardStatus', function () { undefined, serviceProvider ); - configDatabase = new Database(mongo, 'config_test'); + configDatabase = new DatabaseImpl(mongo, 'config_test'); expect(configDatabase.getName()).to.equal('config_test'); const origRunCommandWithCheck = serviceProvider.runCommandWithCheck; @@ -217,7 +217,10 @@ describe('getPrintableShardStatus', function () { configDatabase.getSiblingDB = getSiblingDB; configDatabase._maybeCachedHello = stub().returns({ msg: 'isdbgrid' }); - const status = await getPrintableShardStatus(configDatabase, false); + const status = await getPrintableShardStatus( + configDatabase._typeLaunder(), + false + ); expect(status.shardingVersion.clusterId).to.be.instanceOf(bson.ObjectId); expect(status.shards.map(({ host }: { host: string }) => host)).to.include( 'shard01/localhost:27018,localhost:27019,localhost:27020' @@ -248,7 +251,10 @@ describe('getPrintableShardStatus', function () { 'upgradeState', ]) { it(`does not show ${hiddenField} in shardingVersion`, async function () { - const status = await getPrintableShardStatus(configDatabase, false); + const status = await getPrintableShardStatus( + configDatabase._typeLaunder(), + false + ); expect((status.shardingVersion as any)[hiddenField]).to.equal( undefined ); @@ -259,19 +265,28 @@ describe('getPrintableShardStatus', function () { it('returns whether the balancer is currently running', async function () { { inBalancerRound = true; - const status = await getPrintableShardStatus(configDatabase, true); + const status = await getPrintableShardStatus( + configDatabase._typeLaunder(), + true + ); expect(status.balancer['Currently running']).to.equal('yes'); } { inBalancerRound = false; - const status = await getPrintableShardStatus(configDatabase, true); + const status = await getPrintableShardStatus( + configDatabase._typeLaunder(), + true + ); expect(status.balancer['Currently running']).to.equal('no'); } }); it('returns an object with verbose sharding information if requested', async function () { - const status = await getPrintableShardStatus(configDatabase, true); + const status = await getPrintableShardStatus( + configDatabase._typeLaunder(), + true + ); expect((status['most recently active mongoses'][0] as any).up).to.be.a( 'number' ); @@ -285,7 +300,10 @@ describe('getPrintableShardStatus', function () { _id: 'balancer', activeWindow: { start: '00:00', stop: '23:59' }, }); - const status = await getPrintableShardStatus(configDatabase, false); + const status = await getPrintableShardStatus( + configDatabase._typeLaunder(), + false + ); expect(status.balancer['Balancer active window is set between']).to.equal( '00:00 and 23:59 server local time' ); @@ -301,7 +319,10 @@ describe('getPrintableShardStatus', function () { what: 'balancer.round', ns: '', }); - const status = await getPrintableShardStatus(configDatabase, false); + const status = await getPrintableShardStatus( + configDatabase._typeLaunder(), + false + ); expect( status.balancer['Failed balancer rounds in last 5 attempts'] ).to.equal(1); @@ -315,7 +336,10 @@ describe('getPrintableShardStatus', function () { ts: new bson.ObjectId('5fce116c579db766a198a176'), when: new Date('2020-12-07T11:26:36.803Z'), }); - const status = await getPrintableShardStatus(configDatabase, false); + const status = await getPrintableShardStatus( + configDatabase._typeLaunder(), + false + ); expect( status.balancer['Collections with active migrations'] ).to.have.lengthOf(1); @@ -330,7 +354,10 @@ describe('getPrintableShardStatus', function () { what: 'moveChunk.from', details: { from: 'shard0', to: 'shard1', note: 'success' }, }); - const status = await getPrintableShardStatus(configDatabase, false); + const status = await getPrintableShardStatus( + configDatabase._typeLaunder(), + false + ); expect( status.balancer['Migration Results for the last 24 hours'] ).to.deep.equal({ 1: 'Success' }); @@ -342,7 +369,10 @@ describe('getPrintableShardStatus', function () { what: 'moveChunk.from', details: { from: 'shard0', to: 'shard1', errmsg: 'oopsie' }, }); - const status = await getPrintableShardStatus(configDatabase, false); + const status = await getPrintableShardStatus( + configDatabase._typeLaunder(), + false + ); expect( status.balancer['Migration Results for the last 24 hours'] @@ -352,7 +382,7 @@ describe('getPrintableShardStatus', function () { it('fails when config.version is empty', async function () { await configDatabase.getCollection('version').drop(); try { - await getPrintableShardStatus(configDatabase, false); + await getPrintableShardStatus(configDatabase._typeLaunder(), false); } catch (err: any) { expect(err.name).to.equal('MongoshInvalidInputError'); return; diff --git a/packages/shell-api/src/helpers.ts b/packages/shell-api/src/helpers.ts index 69ec523e08..75e169eaaa 100644 --- a/packages/shell-api/src/helpers.ts +++ b/packages/shell-api/src/helpers.ts @@ -1307,6 +1307,16 @@ export function buildConfigChunksCollectionMatch( : { ns: configCollectionsInfo._id }; // old format } +export interface GenericCollectionSchema { + schema: Document; +} +export interface GenericDatabaseSchema { + [key: string]: GenericCollectionSchema; +} +export interface GenericServerSideSchema { + [key: string]: GenericDatabaseSchema; +} +export type StringKey = keyof T & string; export const aggregateBackgroundOptionNotSupportedHelp = 'the background option is not supported by the aggregate method and will be ignored, ' + 'use runCommand to use { background: true } with Atlas Data Federation'; diff --git a/packages/shell-api/src/index.ts b/packages/shell-api/src/index.ts index e5f0fb4066..076203c9ea 100644 --- a/packages/shell-api/src/index.ts +++ b/packages/shell-api/src/index.ts @@ -1,8 +1,8 @@ import AggregationCursor from './aggregation-cursor'; import RunCommandCursor from './run-command-cursor'; -import Collection from './collection'; +import Collection, { CollectionImpl } from './collection'; import Cursor from './cursor'; -import Database, { CollectionNamesWithTypes } from './database'; +import Database, { DatabaseImpl, CollectionNamesWithTypes } from './database'; import Explainable from './explainable'; import ExplainableCursor from './explainable-cursor'; import Help, { HelpProperties } from './help'; @@ -42,7 +42,9 @@ export { Cursor, CursorIterationResult, Database, + DatabaseImpl, Collection, + CollectionImpl, Explainable, ExplainableCursor, Help, diff --git a/packages/shell-api/src/integration.spec.ts b/packages/shell-api/src/integration.spec.ts index 58f15ce4e0..f664e44370 100644 --- a/packages/shell-api/src/integration.spec.ts +++ b/packages/shell-api/src/integration.spec.ts @@ -3053,7 +3053,7 @@ describe('Shell API (integration)', function () { expect(events.length).to.be.greaterThan(1); expect(events[0]).to.deep.equal({ method: 'printShardingStatus', - class: 'Database', + class: 'DatabaseImpl', deprecated: false, callDepth: 0, isAsync: true, diff --git a/packages/shell-api/src/interruptor.spec.ts b/packages/shell-api/src/interruptor.spec.ts index c378a28fe5..ba9dc2d447 100644 --- a/packages/shell-api/src/interruptor.spec.ts +++ b/packages/shell-api/src/interruptor.spec.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; import type { EventEmitter } from 'events'; import type { StubbedInstance } from 'ts-sinon'; import { stubInterface } from 'ts-sinon'; -import Database from './database'; +import { DatabaseImpl } from './database'; import Mongo from './mongo'; import { InterruptFlag, MongoshInterruptedError } from './interruptor'; import ShellInstanceState from './shell-instance-state'; @@ -51,7 +51,7 @@ describe('interruptor', function () { describe('with Shell API functions', function () { let mongo: Mongo; let serviceProvider: StubbedInstance; - let database: Database; + let database: DatabaseImpl; let bus: StubbedInstance; let instanceState: ShellInstanceState; @@ -70,7 +70,7 @@ describe('interruptor', function () { undefined, serviceProvider ); - database = new Database(mongo, 'db1'); + database = new DatabaseImpl(mongo, 'db1'); }); it('causes an interrupt error to be thrown on entry', async function () { diff --git a/packages/shell-api/src/mongo-errors.spec.ts b/packages/shell-api/src/mongo-errors.spec.ts index 3b028f277d..a13d92cbce 100644 --- a/packages/shell-api/src/mongo-errors.spec.ts +++ b/packages/shell-api/src/mongo-errors.spec.ts @@ -5,10 +5,10 @@ import type { StubbedInstance } from 'ts-sinon'; import { stubInterface } from 'ts-sinon'; import type { ServiceProvider } from '@mongosh/service-provider-core'; import { bson } from '@mongosh/service-provider-core'; -import Database from './database'; +import { DatabaseImpl } from './database'; import type { EventEmitter } from 'events'; import ShellInstanceState from './shell-instance-state'; -import Collection from './collection'; +import { CollectionImpl } from './collection'; class MongoError extends Error { code?: number; @@ -78,10 +78,10 @@ describe('mongo-errors', function () { describe('intercepts shell API calls', function () { let mongo: Mongo; let serviceProvider: StubbedInstance; - let database: Database; + let database: DatabaseImpl; let bus: StubbedInstance; let instanceState: ShellInstanceState; - let collection: Collection; + let collection: CollectionImpl; beforeEach(function () { bus = stubInterface(); @@ -98,8 +98,8 @@ describe('mongo-errors', function () { undefined, serviceProvider ); - database = new Database(mongo, 'db1'); - collection = new Collection(mongo, database, 'coll1'); + database = new DatabaseImpl(mongo, 'db1'); + collection = new CollectionImpl(mongo, database._typeLaunder(), 'coll1'); }); it('on collection.find error', async function () { diff --git a/packages/shell-api/src/mongo.spec.ts b/packages/shell-api/src/mongo.spec.ts index 303a57dfec..3c993c5da7 100644 --- a/packages/shell-api/src/mongo.spec.ts +++ b/packages/shell-api/src/mongo.spec.ts @@ -17,10 +17,10 @@ import type { WriteConcern, } from '@mongosh/service-provider-core'; import { bson } from '@mongosh/service-provider-core'; -import type Database from './database'; +import type { Database, DatabaseImpl } from './database'; import { EventEmitter } from 'events'; import ShellInstanceState from './shell-instance-state'; -import Collection from './collection'; +import { CollectionImpl } from './collection'; import type Cursor from './cursor'; import ChangeStreamCursor from './change-stream-cursor'; import NoDatabase from './no-db'; @@ -99,7 +99,7 @@ describe('Mongo', function () { const driverSession = { driverSession: 1 }; let mongo: Mongo; let serviceProvider: StubbedInstance; - let database: StubbedInstance; + let database: StubbedInstance; let bus: StubbedInstance; let instanceState: ShellInstanceState; @@ -118,8 +118,8 @@ describe('Mongo', function () { undefined, serviceProvider ); - database = stubInterface(); - instanceState.currentDb = database; + database = stubInterface(); + instanceState.currentDb = database as unknown as Database; }); describe('show', function () { it('should send telemetry by default', async function () { @@ -311,7 +311,7 @@ describe('Mongo', function () { }); describe('profile', function () { it('calls database.count but not find when count < 1', async function () { - const syscoll = stubInterface(); + const syscoll = stubInterface(); database.getCollection.returns(syscoll); syscoll.countDocuments.resolves(0); syscoll.find.rejects(new Error()); @@ -325,7 +325,7 @@ describe('Mongo', function () { }); it('calls database.count and find when count > 0', async function () { const expectedResult = [{ a: 'a' }, { b: 'b' }]; - const syscoll = stubInterface(); + const syscoll = stubInterface(); const cursor = stubInterface(); cursor.sort.returns(cursor); cursor.limit.returns(cursor); @@ -349,7 +349,7 @@ describe('Mongo', function () { }); it('throws if collection.find throws', async function () { - const syscoll = stubInterface(); + const syscoll = stubInterface(); database.getCollection.returns(syscoll); syscoll.countDocuments.resolves(1); const expectedError = new Error(); @@ -358,7 +358,7 @@ describe('Mongo', function () { expect(caughtError).to.equal(expectedError); }); it('throws if collection.countDocuments rejects', async function () { - const syscoll = stubInterface(); + const syscoll = stubInterface(); database.getCollection.returns(syscoll); const expectedError = new Error(); syscoll.countDocuments.rejects(expectedError); @@ -928,26 +928,28 @@ describe('Mongo', function () { describe('getCollection', function () { it('returns a collection for the database', function () { const coll = mongo.getCollection('db1.coll'); - expect(coll).to.be.instanceOf(Collection); + expect(coll).to.be.instanceOf(CollectionImpl); expect(coll._name).to.equal('coll'); expect(coll._database._name).to.equal('db1'); }); it('returns a collection for the database with multiple .', function () { const coll = mongo.getCollection('db1.coll.subcoll'); - expect(coll).to.be.instanceOf(Collection); + expect(coll).to.be.instanceOf(CollectionImpl); expect(coll._name).to.equal('coll.subcoll'); expect(coll._database._name).to.equal('db1'); }); - it('throws if name is not a valid connection string', function () { + it('throws if name is not a valid collnection string', function () { expect(() => { + // @ts-expect-error db is not valid, but that's the point of the test mongo.getCollection('db'); }).to.throw('Collection must be of the format .'); }); it('throws if name is empty', function () { expect(() => { + // @ts-expect-error db is not valid, but that's the point of the test mongo.getCollection(''); }).to.throw('Collection must be of the format .'); }); diff --git a/packages/shell-api/src/mongo.ts b/packages/shell-api/src/mongo.ts index 193b91c0d8..4aadf57a6e 100644 --- a/packages/shell-api/src/mongo.ts +++ b/packages/shell-api/src/mongo.ts @@ -44,14 +44,15 @@ import { mapCliToDriver, generateConnectionInfoFromCliArgs, } from '@mongosh/arg-parser'; -import type Collection from './collection'; -import Database from './database'; +import type { Database } from './database'; +import { DatabaseImpl } from './database'; import type ShellInstanceState from './shell-instance-state'; import { ClientBulkWriteResult } from './result'; import { CommandResult } from './result'; import { redactURICredentials } from '@mongosh/history'; import { asPrintable, ServerVersions, Topologies } from './enums'; import Session from './session'; +import type { GenericServerSideSchema, StringKey } from './helpers'; import { assertArgsDefinedType, processFLEOptions, @@ -64,6 +65,7 @@ import { KeyVault, ClientEncryption } from './field-level-encryption'; import { ShellApiErrors } from './error-codes'; import type { LogEntry } from './log-entry'; import { parseAnyLogEntry } from './log-entry'; +import type { Collection } from './collection'; import type { ShellBson } from './shell-bson'; /* Utility, inverse of Readonly */ @@ -73,16 +75,19 @@ type Mutable = { @shellApiClassDefault @classPlatforms(['CLI']) -export default class Mongo extends ShellApiClass { +export default class Mongo< + M extends GenericServerSideSchema = GenericServerSideSchema +> extends ShellApiClass { private __serviceProvider: ServiceProvider | null = null; - public readonly _databases: Record = Object.create(null); + public readonly _databases: Record, Database> = + Object.create(null); public _instanceState: ShellInstanceState; public _connectionInfo: ConnectionInfo; private _explicitEncryptionOnly = false; private _keyVault: KeyVault | undefined; // need to keep it around so that the ShellApi ClientEncryption class can access it private _clientEncryption: ClientEncryption | undefined; private _readPreferenceWasExplicitlyRequested = false; - private _cachedDatabaseNames: string[] = []; + private _cachedDatabaseNames: StringKey[] = []; constructor( instanceState: ShellInstanceState, @@ -252,7 +257,7 @@ export default class Mongo extends ShellApiClass { } } - _getDb(name: string): Database { + _getDb>(name: K): Database { assertArgsDefinedType([name], ['string']); if (!isValidDatabaseName(name)) { throw new MongoshInvalidInputError( @@ -262,20 +267,22 @@ export default class Mongo extends ShellApiClass { } if (!(name in this._databases)) { - this._databases[name] = new Database(this, name); + this._databases[name] = new DatabaseImpl(this, name)._typeLaunder(); } - return this._databases[name]; + return this._databases[name] as Database; } @returnType('Database') - getDB(db: string): Database { + getDB>(db: K): Database { assertArgsDefinedType([db], ['string'], 'Mongo.getDB'); this._instanceState.messageBus.emit('mongosh:getDB', { db }); return this._getDb(db); } @returnType('Collection') - getCollection(name: string): Collection { + getCollection, KC extends StringKey>( + name: `${KD}.${KC}` + ): Collection { assertArgsDefinedType([name], ['string']); const { db, coll } = /^(?[^.]+)\.(?.+)$/.exec(name)?.groups ?? {}; if (!db || !coll) { @@ -284,14 +291,18 @@ export default class Mongo extends ShellApiClass { CommonErrors.InvalidArgument ); } - return this._getDb(db).getCollection(coll); + return this._getDb(db as StringKey).getCollection(coll) as Collection< + M, + M[KD], + M[KD][KC] + >; } getURI(): string { return this._uri; } - use(db: string): string { + use(db: StringKey): string { assertArgsDefinedType([db], ['string'], 'Mongo.use'); this._instanceState.messageBus.emit('mongosh:use', { db }); @@ -404,9 +415,13 @@ export default class Mongo extends ShellApiClass { @returnsPromise @apiVersions([1]) - async getDBNames(options: ListDatabasesOptions = {}): Promise { + async getDBNames( + options: ListDatabasesOptions = {} + ): Promise[]> { this._emitMongoApiCall('getDBNames', { options }); - return (await this._listDatabases(options)).databases.map((db) => db.name); + return (await this._listDatabases(options)).databases.map( + (db) => db.name as StringKey + ); } @returnsPromise @@ -883,17 +898,23 @@ export default class Mongo extends ShellApiClass { for (const approach of [ // Try $documents if available (NB: running $documents on an empty db requires SERVER-63811 i.e. 6.0.3+). () => - this.getDB('_fakeDbForMongoshCSKTH').aggregate([ + this.getDB('_fakeDbForMongoshCSKTH' as StringKey).aggregate([ + { $documents: [{}] }, + ...pipeline, + ]), + () => + this.getDB('admin' as StringKey).aggregate([ { $documents: [{}] }, ...pipeline, ]), - () => this.getDB('admin').aggregate([{ $documents: [{}] }, ...pipeline]), // If that fails, try a default collection like admin.system.version. () => - this.getDB('admin').getCollection('system.version').aggregate(pipeline), + this.getDB('admin' as StringKey) + .getCollection('system.version') + .aggregate(pipeline), // If that fails, try using $collStats for local.oplog.rs. () => - this.getDB('local') + this.getDB('local' as StringKey) .getCollection('oplog.rs') .aggregate([{ $collStats: {} }, ...pipeline]), ]) { diff --git a/packages/shell-api/src/replica-set.spec.ts b/packages/shell-api/src/replica-set.spec.ts index f03129b3c1..c3b1223ced 100644 --- a/packages/shell-api/src/replica-set.spec.ts +++ b/packages/shell-api/src/replica-set.spec.ts @@ -23,7 +23,7 @@ import { skipIfApiStrict, } from '../../../testing/integration-testing-hooks'; import { NodeDriverServiceProvider } from '../../service-provider-node-driver'; -import Database from './database'; +import { DatabaseImpl } from './database'; import { ADMIN_DB, ALL_PLATFORMS, @@ -94,7 +94,7 @@ describe('ReplicaSet', function () { let rs: ReplicaSet; let bus: StubbedInstance; let instanceState: ShellInstanceState; - let db: Database; + let db: DatabaseImpl; beforeEach(function () { bus = stubInterface(); @@ -113,8 +113,8 @@ describe('ReplicaSet', function () { undefined, serviceProvider ); - db = new Database(mongo, 'testdb'); - rs = new ReplicaSet(db); + db = new DatabaseImpl(mongo, 'testdb'); + rs = new ReplicaSet(db._typeLaunder()); }); describe('initiate', function () { @@ -832,7 +832,7 @@ describe('ReplicaSet', function () { let additionalServer: MongodSetup; let serviceProvider: NodeDriverServiceProvider; let instanceState: ShellInstanceState; - let db: Database; + let db: DatabaseImpl; let rs: ReplicaSet; before(async function () { @@ -855,7 +855,7 @@ describe('ReplicaSet', function () { ); instanceState = new ShellInstanceState(serviceProvider); db = instanceState.currentDb; - rs = new ReplicaSet(db); + rs = new ReplicaSet(db._typeLaunder()); // check replset uninitialized try { diff --git a/packages/shell-api/src/replica-set.ts b/packages/shell-api/src/replica-set.ts index cb0b0872df..3c3177c81c 100644 --- a/packages/shell-api/src/replica-set.ts +++ b/packages/shell-api/src/replica-set.ts @@ -18,6 +18,7 @@ import { import { asPrintable } from './enums'; import { assertArgsDefinedType } from './helpers'; import type { CommandResult } from './result'; +import type { GenericDatabaseSchema, GenericServerSideSchema } from './helpers'; export type ReplSetMemberConfig = { _id: number; @@ -35,15 +36,18 @@ export type ReplSetConfig = { }; @shellApiClassDefault -export default class ReplicaSet extends ShellApiWithMongoClass { - _database: Database; +export default class ReplicaSet< + M extends GenericServerSideSchema = GenericServerSideSchema, + D extends GenericDatabaseSchema = GenericDatabaseSchema +> extends ShellApiWithMongoClass { + _database: Database; - constructor(database: Database) { + constructor(database: Database) { super(); this._database = database; } - get _mongo(): Mongo { + get _mongo(): Mongo { return this._database._mongo; } diff --git a/packages/shell-api/src/session.spec.ts b/packages/shell-api/src/session.spec.ts index f3003caeb7..f7bb79185e 100644 --- a/packages/shell-api/src/session.spec.ts +++ b/packages/shell-api/src/session.spec.ts @@ -25,7 +25,7 @@ import { skipIfApiStrict, } from '../../../testing/integration-testing-hooks'; import { ensureMaster, ensureSessionExists } from '../test/helpers'; -import Database from './database'; +import { DatabaseImpl } from './database'; import { CommonErrors, MongoshInvalidInputError } from '@mongosh/errors'; import { EventEmitter } from 'events'; import { dummyOptions } from './helpers.spec'; @@ -101,12 +101,12 @@ describe('Session', function () { describe('getDatabase', function () { it('works for a regular database', function () { const db = session.getDatabase('test'); - expect(db).to.deep.equal(new Database(mongo, 'test', session)); + expect(db).to.deep.equal(new DatabaseImpl(mongo, 'test', session)); expect(session.getDatabase('test')).to.equal(db); // reuses db }); it('also affects Database.getSiblingDB', function () { const db = session.getDatabase('othername').getSiblingDB('test'); - expect(db).to.deep.equal(new Database(mongo, 'test', session)); + expect(db).to.deep.equal(new DatabaseImpl(mongo, 'test', session)); expect(session.getDatabase('test')).to.equal(db); // reuses db }); it('throws for an invalid name', function () { diff --git a/packages/shell-api/src/session.ts b/packages/shell-api/src/session.ts index cdae162d54..cd6e062014 100644 --- a/packages/shell-api/src/session.ts +++ b/packages/shell-api/src/session.ts @@ -14,21 +14,25 @@ import type { } from '@mongosh/service-provider-core'; import { asPrintable } from './enums'; import type Mongo from './mongo'; -import Database from './database'; +import type Database from './database'; +import { DatabaseImpl } from './database'; import { CommonErrors, MongoshInvalidInputError } from '@mongosh/errors'; +import type { GenericServerSideSchema, StringKey } from './helpers'; import { assertArgsDefinedType, isValidDatabaseName } from './helpers'; @shellApiClassDefault @classPlatforms(['CLI']) -export default class Session extends ShellApiWithMongoClass { +export default class Session< + M extends GenericServerSideSchema = GenericServerSideSchema +> extends ShellApiWithMongoClass { public id: ServerSessionId | undefined; public _session: ClientSession; public _options: ClientSessionOptions; - public _mongo: Mongo; - private _databases: Record; + public _mongo: Mongo; + private _databases: Record>; constructor( - mongo: Mongo, + mongo: Mongo, options: ClientSessionOptions, session: ClientSession ) { @@ -47,7 +51,7 @@ export default class Session extends ShellApiWithMongoClass { return this._session.id; } - getDatabase(name: string): Database { + getDatabase>(name: K): Database { assertArgsDefinedType([name], ['string'], 'Session.getDatabase'); if (!isValidDatabaseName(name)) { @@ -58,9 +62,13 @@ export default class Session extends ShellApiWithMongoClass { } if (!(name in this._databases)) { - this._databases[name] = new Database(this._mongo, name, this); + this._databases[name] = new DatabaseImpl( + this._mongo, + name, + this + )._typeLaunder(); } - return this._databases[name]; + return this._databases[name] as Database; } advanceOperationTime(ts: TimestampType): void { diff --git a/packages/shell-api/src/shard.spec.ts b/packages/shell-api/src/shard.spec.ts index cfc6fd37fa..9a564e1046 100644 --- a/packages/shell-api/src/shard.spec.ts +++ b/packages/shell-api/src/shard.spec.ts @@ -29,7 +29,8 @@ import { skipIfServerVersion, skipIfApiStrict, } from '../../../testing/integration-testing-hooks'; -import Database from './database'; +import type Database from './database'; +import { DatabaseImpl } from './database'; import { inspect } from 'util'; import { dummyOptions } from './helpers.spec'; @@ -116,7 +117,7 @@ describe('Shard', function () { undefined, serviceProvider ); - db = new Database(mongo, 'testDb'); + db = new DatabaseImpl(mongo, 'testDb')._typeLaunder(); shard = new Shard(db); }); describe('enableSharding', function () { diff --git a/packages/shell-api/src/shard.ts b/packages/shell-api/src/shard.ts index be2a4e250e..58558cb514 100644 --- a/packages/shell-api/src/shard.ts +++ b/packages/shell-api/src/shard.ts @@ -12,7 +12,12 @@ import type { Document, CheckMetadataConsistencyOptions, } from '@mongosh/service-provider-core'; -import type { ShardInfo, ShardingStatusResult } from './helpers'; +import type { + ShardInfo, + ShardingStatusResult, + GenericDatabaseSchema, + GenericServerSideSchema, +} from './helpers'; import { assertArgsDefinedType, getConfigDB, @@ -28,15 +33,18 @@ import type RunCommandCursor from './run-command-cursor'; import semver from 'semver'; @shellApiClassDefault -export default class Shard extends ShellApiWithMongoClass { - _database: Database; +export default class Shard< + M extends GenericServerSideSchema = GenericServerSideSchema, + D extends GenericDatabaseSchema = GenericDatabaseSchema +> extends ShellApiWithMongoClass { + _database: Database; - constructor(database: Database) { + constructor(database: Database) { super(); this._database = database; } - get _mongo(): Mongo { + get _mongo(): Mongo { return this._database._mongo; } @@ -205,7 +213,7 @@ export default class Shard extends ShellApiWithMongoClass { @apiVersions([1]) async status( verbose = false, - configDB?: Database + configDB?: Database ): Promise> { const result = await getPrintableShardStatus( configDB ?? (await getConfigDB(this._database)), diff --git a/packages/shell-api/src/shell-api.spec.ts b/packages/shell-api/src/shell-api.spec.ts index 94387193cc..d86395bc5d 100644 --- a/packages/shell-api/src/shell-api.spec.ts +++ b/packages/shell-api/src/shell-api.spec.ts @@ -496,7 +496,7 @@ describe('ShellApi', function () { 'username', 'pwd' ); - expect((await toShellResult(db)).type).to.equal('Database'); + expect((await toShellResult(db)).type).to.equal('DatabaseImpl'); expect(db.getMongo()._uri).to.equal( 'mongodb://username:pwd@localhost:27017/?directConnection=true&serverSelectionTimeoutMS=2000' ); @@ -624,7 +624,7 @@ describe('ShellApi', function () { const db = await instanceState.context.connect( 'mongodb://127.0.0.1:27017' ); - expect((await toShellResult(db)).type).to.equal('Database'); + expect((await toShellResult(db)).type).to.equal('DatabaseImpl'); expect(db.getMongo()._uri).to.equal( 'mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000' ); @@ -636,7 +636,7 @@ describe('ShellApi', function () { 'username', 'pwd' ); - expect((await toShellResult(db)).type).to.equal('Database'); + expect((await toShellResult(db)).type).to.equal('DatabaseImpl'); expect(db.getMongo()._uri).to.equal( 'mongodb://username:pwd@127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000' ); diff --git a/packages/shell-api/src/shell-instance-state.spec.ts b/packages/shell-api/src/shell-instance-state.spec.ts index 9889f5683e..3fdd909c7d 100644 --- a/packages/shell-api/src/shell-instance-state.spec.ts +++ b/packages/shell-api/src/shell-instance-state.spec.ts @@ -43,7 +43,7 @@ describe('ShellInstanceState', function () { it('provides printing ability for shell API objects', async function () { await run('print(db)'); expect(evaluationListener.onPrint?.lastCall.args[0][0].type).to.equal( - 'Database' + 'DatabaseImpl' ); }); diff --git a/packages/shell-api/src/shell-instance-state.ts b/packages/shell-api/src/shell-instance-state.ts index 0ce2d93fc7..1a867479d4 100644 --- a/packages/shell-api/src/shell-instance-state.ts +++ b/packages/shell-api/src/shell-instance-state.ts @@ -142,7 +142,7 @@ const CONTROL_CHAR_REGEXP = /[\x00-\x1F\x7F-\x9F]/g; * shell API is concerned) and keeps track of all open connections (a.k.a. Mongo * instances). */ -export default class ShellInstanceState { +export class ShellInstanceState { public currentCursor: | Cursor | AggregationCursor @@ -352,7 +352,7 @@ export default class ShellInstanceState { contextObject.sp = Streams.newInstance(this.currentDb); const setFunc = (newDb: any): Database => { - if (getShellApiType(newDb) !== 'Database') { + if (getShellApiType(newDb) !== 'DatabaseImpl') { throw new MongoshInvalidInputError( "Cannot reassign 'db' to non-Database type", CommonErrors.InvalidOperation @@ -728,3 +728,5 @@ export default class ShellInstanceState { } } } + +export default ShellInstanceState; diff --git a/packages/shell-api/src/stream-processor.ts b/packages/shell-api/src/stream-processor.ts index 789fc9dd74..0a436d05c7 100644 --- a/packages/shell-api/src/stream-processor.ts +++ b/packages/shell-api/src/stream-processor.ts @@ -12,7 +12,7 @@ import { import type { Streams } from './streams'; @shellApiClassDefault -export default class StreamProcessor extends ShellApiWithMongoClass { +export class StreamProcessor extends ShellApiWithMongoClass { constructor(public _streams: Streams, public name: string) { super(); } @@ -152,3 +152,4 @@ export default class StreamProcessor extends ShellApiWithMongoClass { return; } } +export default StreamProcessor; diff --git a/packages/shell-api/src/streams.spec.ts b/packages/shell-api/src/streams.spec.ts index 8a26b6d71b..5a57094edd 100644 --- a/packages/shell-api/src/streams.spec.ts +++ b/packages/shell-api/src/streams.spec.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import type Mongo from './mongo'; -import Database from './database'; +import { DatabaseImpl } from './database'; import { Streams } from './streams'; import { InterruptFlag, MongoshInterruptedError } from './interruptor'; import type { MongoshInvalidInputError } from '@mongosh/errors'; @@ -23,7 +23,8 @@ describe('Streams', function () { runCommand: identity, }, } as unknown as Mongo; - streams = new Streams(new Database(mongo, 'testDb')); + const db = new DatabaseImpl(mongo, 'testDb'); + streams = new Streams(db._typeLaunder()); }); describe('createStreamProcessor', function () { diff --git a/packages/shell-api/src/streams.ts b/packages/shell-api/src/streams.ts index 127677148b..73ef6e1656 100644 --- a/packages/shell-api/src/streams.ts +++ b/packages/shell-api/src/streams.ts @@ -10,11 +10,18 @@ import StreamProcessor from './stream-processor'; import { ADMIN_DB, asPrintable, shellApiType } from './enums'; import type Database from './database'; import type Mongo from './mongo'; +import type { GenericDatabaseSchema, GenericServerSideSchema } from './helpers'; @shellApiClassDefault -export class Streams extends ShellApiWithMongoClass { - public static newInstance(database: Database) { - return new Proxy(new Streams(database), { +export class Streams< + M extends GenericServerSideSchema = GenericServerSideSchema, + D extends GenericDatabaseSchema = GenericDatabaseSchema +> extends ShellApiWithMongoClass { + public static newInstance< + M extends GenericServerSideSchema = GenericServerSideSchema, + D extends GenericDatabaseSchema = GenericDatabaseSchema + >(database: Database) { + return new Proxy(new Streams(database), { get(target, prop) { const v = (target as any)[prop]; if (v !== undefined) { @@ -27,14 +34,14 @@ export class Streams extends ShellApiWithMongoClass { }); } - private _database: Database; + private _database: Database; - constructor(database: Database) { + constructor(database: Database) { super(); this._database = database; } - get _mongo(): Mongo { + get _mongo(): Mongo { return this._database._mongo; } @@ -42,7 +49,7 @@ export class Streams extends ShellApiWithMongoClass { return 'Atlas Stream Processing'; } - getProcessor(name: string) { + getProcessor(name: string): StreamProcessor { return new StreamProcessor(this, name); }