diff --git a/package-lock.json b/package-lock.json index 7d22747fb7..eb1779846f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6114,6 +6114,126 @@ "node": ">=8" } }, + "node_modules/@microsoft/api-extractor": { + "version": "7.52.8", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.8.tgz", + "integrity": "sha512-cszYIcjiNscDoMB1CIKZ3My61+JOhpERGlGr54i6bocvGLrcL/wo9o+RNXMBrb7XgLtKaizZWUpqRduQuHQLdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor-model": "7.30.6", + "@microsoft/tsdoc": "~0.15.1", + "@microsoft/tsdoc-config": "~0.17.1", + "@rushstack/node-core-library": "5.13.1", + "@rushstack/rig-package": "0.5.3", + "@rushstack/terminal": "0.15.3", + "@rushstack/ts-command-line": "5.0.1", + "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.6", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.6.tgz", + "integrity": "sha512-znmFn69wf/AIrwHya3fxX6uB5etSIn6vg4Q4RB/tb5VDDs1rqREc+AvMC/p19MUN13CZ7+V/8pkYPTj7q8tftg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "~0.15.1", + "@microsoft/tsdoc-config": "~0.17.1", + "@rushstack/node-core-library": "5.13.1" + } + }, + "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", @@ -9310,6 +9430,163 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/@rushstack/node-core-library": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.13.1.tgz", + "integrity": "sha512-5yXhzPFGEkVc9Fu92wsNJ9jlvdwz4RNb2bMso+/+TH0nMm1jDDDsOIf4l8GAkPxGuwPw5DH24RliWVfSPhlW/Q==", + "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.3", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.3.tgz", + "integrity": "sha512-DGJ0B2Vm69468kZCJkPj3AH5nN+nR9SPmC0rFHtzsS4lBQ7/dgOwtwVxYP7W9JPDMuRBkJ4KHmWKr036eJsj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/node-core-library": "5.13.1", + "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": "5.0.1", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.0.1.tgz", + "integrity": "sha512-bsbUucn41UXrQK7wgM8CNM/jagBytEyJqXw/umtI8d68vFm1Jwxh1OtLrlW7uGZgjCWiiPH6ooUNa1aVsuVr3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/terminal": "0.15.3", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, "node_modules/@segment/analytics-core": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@segment/analytics-core/-/analytics-core-1.4.1.tgz", @@ -10748,6 +11025,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", @@ -18540,9 +18824,9 @@ "license": "MIT" }, "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "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", @@ -20099,6 +20383,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", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -21264,6 +21558,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", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", @@ -30596,6 +30897,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", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -31746,9 +32057,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "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", @@ -34564,6 +34875,8 @@ "version": "3.10.3", "license": "Apache-2.0", "dependencies": { + "@babel/core": "^7.26.10", + "@babel/types": "^7.26.10", "@mongosh/arg-parser": "^3.10.2", "@mongosh/errors": "2.4.0", "@mongosh/history": "2.4.6", @@ -34572,6 +34885,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/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<ReturnType<typeof execFileWithoutLogging>> { 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..93d5226dd8 100644 --- a/packages/cli-repl/src/cli-repl.spec.ts +++ b/packages/cli-repl/src/cli-repl.spec.ts @@ -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..e85014822d --- /dev/null +++ b/packages/shell-api/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "<projectFolder>/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" + ], + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "", + "publicTrimmedFilePath": "<projectFolder>/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": "none" + }, + "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..0e1c6ad10f --- /dev/null +++ b/packages/shell-api/bin/api-postprocess.ts @@ -0,0 +1,162 @@ +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 '../'; +import enUs from '../../i18n/src/locales/en_US'; + +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]); + + const classHelp = (enUs['shell-api'] as any).classes[className]; + if ( + classHelp && + classHelp.help.attributes && + classHelp.help.attributes[methodName] + ) { + const methodHelp = classHelp.help.attributes[methodName]; + if (methodHelp && methodHelp.description) { + //console.log(`${className}.${methodName}`, methodHelp.description); + path.addComment( + 'leading', + `\n${methodHelp.description as string}\n`, + false + ); + } + } + + 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() { + // eslint-disable-next-line no-console + console.log('Postprocessing lib/api-raw.d.ts as lib/api-processed.d.ts...'); + 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 apiGlobals = await fs.readFile( + path.resolve(__dirname, '..', 'src', 'api-globals.d.ts'), + 'utf8' + ); + + const code = (result?.code ?? '') + '\n' + apiGlobals; + await fs.writeFile( + path.resolve(__dirname, '..', 'lib', 'api-processed.d.ts'), + code + ); + + // eslint-disable-next-line no-console + console.log('Writing lib/api-export.js...'); + const exportCode = `"use strict"; +module.exports = { api: ${JSON.stringify(code)} }; +`; + await fs.writeFile( + path.resolve(__dirname, '..', 'lib', 'api-export.js'), + exportCode + ); + + // eslint-disable-next-line no-console + console.log('Writing lib/api-export.d.ts...'); + await fs.writeFile( + path.resolve(__dirname, '..', 'lib', 'api-export.d.ts'), + 'export declare const api;' + ); +} + +main().catch((err) => + process.nextTick(() => { + throw err; + }) +); diff --git a/packages/shell-api/package.json b/packages/shell-api/package.json index 617089d133..30e8681757 100644 --- a/packages/shell-api/package.json +++ b/packages/shell-api/package.json @@ -4,6 +4,16 @@ "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": { + "default": "./lib/api-export.js", + "types": "./lib/api-export.d.ts" + } + }, "config": { "unsafe-perm": true }, @@ -12,7 +22,8 @@ "url": "git://github.com/mongodb-js/mongosh.git" }, "scripts": { - "compile": "tsc -p tsconfig.json", + "compile": "tsc -p tsconfig.json && npm run api-generate", + "api-generate": "api-extractor run && ts-node bin/api-postprocess.ts", "pretest": "npm run compile", "eslint": "eslint", "lint": "npm run eslint . && npm run prettier -- --check .", @@ -40,6 +51,8 @@ "build" ], "dependencies": { + "@babel/core": "^7.26.10", + "@babel/types": "^7.26.10", "@mongosh/arg-parser": "^3.10.2", "@mongosh/errors": "2.4.0", "@mongosh/history": "2.4.6", @@ -48,6 +61,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-globals.d.ts b/packages/shell-api/src/api-globals.d.ts new file mode 100644 index 0000000000..c0b219a32e --- /dev/null +++ b/packages/shell-api/src/api-globals.d.ts @@ -0,0 +1,44 @@ +declare global { + const use: ShellApi['use']; + const show: ShellApi['show']; + const exit: ShellApi['exit']; + const quit: ShellApi['quit']; + const Mongo: ShellApi['Mongo']; + const connect: ShellApi['connect']; + const it: ShellApi['it']; + const version: ShellApi['version']; + const load: ShellApi['load']; + const enableTelemetry: ShellApi['enableTelemetry']; + const disableTelemetry: ShellApi['disableTelemetry']; + const passwordPrompt: ShellApi['passwordPrompt']; + const sleep: ShellApi['sleep']; + const print: ShellApi['print']; + const printjson: ShellApi['printjson']; + const convertShardKeyToHashed: ShellApi['convertShardKeyToHashed']; + const cls: ShellApi['cls']; + const isInteractive: ShellApi['isInteractive']; + + const DBRef: ShellBson['DBRef']; + const bsonsize: ShellBson['bsonsize']; + const MaxKey: ShellBson['MaxKey']; + const MinKey: ShellBson['MinKey']; + const ObjectId: ShellBson['ObjectId']; + const Timestamp: ShellBson['Timestamp']; + const Code: ShellBson['Code']; + const NumberDecimal: ShellBson['NumberDecimal']; + const NumberInt: ShellBson['NumberInt']; + const NumberLong: ShellBson['NumberLong']; + const ISODate: ShellBson['ISODate']; + const BinData: ShellBson['BinData']; + const HexData: ShellBson['HexData']; + const UUID: ShellBson['UUID']; + const MD5: ShellBson['MD5']; + const Decimal128: ShellBson['Decimal128']; + const BSONSymbol: ShellBson['BSONSymbol']; + const Int32: ShellBson['Int32']; + const Long: ShellBson['Long']; + const Binary: ShellBson['Binary']; + const Double: ShellBson['Double']; + const EJSON: ShellBson['EJSON']; + const BSONRegExp: ShellBson['BSONRegExp']; +} diff --git a/packages/shell-api/src/api.ts b/packages/shell-api/src/api.ts new file mode 100644 index 0000000000..5f382258ca --- /dev/null +++ b/packages/shell-api/src/api.ts @@ -0,0 +1,10 @@ +export { CollectionWithSchema } from './collection'; +export { ShellBson } from './shell-bson'; +export { Streams } from './streams'; +export { DatabaseWithSchema } from './database'; +import Shard from './shard'; +import ReplicaSet from './replica-set'; +import ShellApi from './shell-api'; +import Mongo from './mongo'; + +export { Mongo, Shard, ReplicaSet, ShellApi }; diff --git a/packages/shell-api/src/bulk.spec.ts b/packages/shell-api/src/bulk.spec.ts index 54947ead9f..16eb749ccf 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 { Collection } from './collection'; import { ALL_PLATFORMS, ALL_SERVER_VERSIONS, ALL_TOPOLOGIES } from './enums'; import { signatures, toShellResult } from './index'; import { BulkWriteResult } from './result'; diff --git a/packages/shell-api/src/bulk.ts b/packages/shell-api/src/bulk.ts index 366979443e..c289b1414d 100644 --- a/packages/shell-api/src/bulk.ts +++ b/packages/shell-api/src/bulk.ts @@ -20,7 +20,7 @@ import type { import { asPrintable } from './enums'; import { assertArgsDefinedType, shallowClone } from './helpers'; import { BulkWriteResult } from './result'; -import type Collection from './collection'; +import type { CollectionWithSchema } from './collection'; @shellApiClassDefault export class BulkFindOp extends ShellApiWithMongoClass { @@ -131,14 +131,14 @@ export class BulkFindOp extends ShellApiWithMongoClass { @shellApiClassDefault export default class Bulk extends ShellApiWithMongoClass { _mongo: Mongo; - _collection: Collection; + _collection: CollectionWithSchema; _batchCounts: any; _executed: boolean; _serviceProviderBulkOp: OrderedBulkOperation | UnorderedBulkOperation; _ordered: boolean; constructor( - collection: Collection, + collection: CollectionWithSchema, innerBulk: OrderedBulkOperation | UnorderedBulkOperation, ordered = false ) { diff --git a/packages/shell-api/src/change-stream-cursor.spec.ts b/packages/shell-api/src/change-stream-cursor.spec.ts index 83fd198e08..3a92b4227b 100644 --- a/packages/shell-api/src/change-stream-cursor.spec.ts +++ b/packages/shell-api/src/change-stream-cursor.spec.ts @@ -17,8 +17,8 @@ import { NodeDriverServiceProvider } from '../../service-provider-node-driver'; import ShellInstanceState from './shell-instance-state'; import Mongo from './mongo'; import { ensureMaster, ensureResult } from '../test/helpers'; -import type Database from './database'; -import type Collection from './collection'; +import type { DatabaseWithSchema } from './database'; +import type { CollectionWithSchema } from './collection'; import { MongoshUnimplementedError } from '@mongosh/errors'; import { EventEmitter } from 'events'; import { dummyOptions } from './helpers.spec'; @@ -124,8 +124,8 @@ describe('ChangeStreamCursor', function () { let serviceProvider: NodeDriverServiceProvider; let instanceState: ShellInstanceState; let mongo: Mongo; - let db: Database; - let coll: Collection; + let db: DatabaseWithSchema; + let coll: CollectionWithSchema; let cursor: ChangeStreamCursor; before(async function () { diff --git a/packages/shell-api/src/collection.spec.ts b/packages/shell-api/src/collection.spec.ts index 63b7480072..9e7e17ee05 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 { Database } from './database'; import Mongo from './mongo'; -import Collection from './collection'; +import { Collection } from './collection'; import ChangeStreamCursor from './change-stream-cursor'; import Explainable from './explainable'; import type { @@ -33,6 +33,7 @@ import { MongoshInvalidInputError, MongoshRuntimeError, } from '@mongosh/errors'; +import type { StringKey } from './helpers'; const sinonChai = require('sinon-chai'); // weird with import @@ -127,12 +128,23 @@ describe('Collection', function () { }); }); describe('commands', function () { - let mongo: Mongo; + type ServerSchema = { + db1: { + coll1: { + schema: {}; + }; + }; + }; + let mongo: Mongo<ServerSchema>; let serviceProvider: StubbedInstance<ServiceProvider>; - let database: Database; + let database: Database<ServerSchema, ServerSchema['db1']>; let bus: StubbedInstance<EventEmitter>; let instanceState: ShellInstanceState; - let collection: Collection; + let collection: Collection< + ServerSchema, + ServerSchema['db1'], + ServerSchema['db1']['coll1'] + >; beforeEach(function () { bus = stubInterface<EventEmitter>(); @@ -149,8 +161,15 @@ describe('Collection', function () { undefined, serviceProvider ); - database = new Database(mongo, 'db1'); - collection = new Collection(mongo, database, 'coll1'); + database = new Database<ServerSchema, ServerSchema['db1']>( + mongo, + 'db1' as StringKey<ServerSchema> + ); + collection = new Collection< + ServerSchema, + ServerSchema['db1'], + ServerSchema['db1']['coll1'] + >(mongo, database, 'coll1'); }); describe('aggregate', function () { let serviceProviderCursor: StubbedInstance<ServiceProviderAggregationCursor>; @@ -2802,13 +2821,24 @@ describe('Collection', function () { }); describe('fle2', function () { - let mongo1: Mongo; - let mongo2: Mongo; + type ServerSchema = { + db1: { + collfle2: { + schema: {}; + }; + }; + }; + let mongo1: Mongo<ServerSchema>; + let mongo2: Mongo<ServerSchema>; let serviceProvider: StubbedInstance<ServiceProvider>; - let database: Database; + let database: Database<ServerSchema, ServerSchema['db1']>; let bus: StubbedInstance<EventEmitter>; let instanceState: ShellInstanceState; - let collection: Collection; + let collection: Collection< + ServerSchema, + ServerSchema['db1'], + ServerSchema['db1']['collfle2'] + >; let keyId: any[]; beforeEach(function () { bus = stubInterface<EventEmitter>(); @@ -2821,7 +2851,8 @@ describe('Collection', function () { keyId = [ { $binary: { base64: 'oh3caogGQ4Sf34ugKnZ7Xw==', subType: '04' } }, ]; - mongo1 = new Mongo( + + mongo1 = new Mongo<ServerSchema>( instanceState, undefined, { @@ -2836,8 +2867,15 @@ describe('Collection', function () { undefined, serviceProvider ); - database = new Database(mongo1, 'db1'); - collection = new Collection(mongo1, database, 'collfle2'); + database = new Database<ServerSchema, ServerSchema['db1']>( + mongo1, + 'db1' as StringKey<ServerSchema> + ); + collection = new Collection( + mongo1, + database, + 'collfle2' as StringKey<ServerSchema['db1']> + ); mongo2 = new Mongo( instanceState, undefined, diff --git a/packages/shell-api/src/collection.ts b/packages/shell-api/src/collection.ts index 7b7432d77b..f919d0b1f0 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, @@ -70,7 +73,7 @@ import type { AggregateOptions, SearchIndexDescription, } from '@mongosh/service-provider-core'; -import type { RunCommandCursor, Database } from './index'; +import type { RunCommandCursor, Database, DatabaseWithSchema } from './index'; import { AggregationCursor, BulkWriteResult, @@ -94,16 +97,40 @@ import PlanCache from './plan-cache'; import ChangeStreamCursor from './change-stream-cursor'; import { ShellApiErrors } from './error-codes'; +export type CollectionWithSchema< + M extends GenericServerSideSchema = GenericServerSideSchema, + D extends GenericDatabaseSchema = M[keyof M], + C extends GenericCollectionSchema = D[keyof D], + N extends StringKey<D> = StringKey<D> +> = Collection<M, D, C, N> & { + [k in StringKey<D> 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 Collection< + M extends GenericServerSideSchema = GenericServerSideSchema, + D extends GenericDatabaseSchema = M[keyof M], + C extends GenericCollectionSchema = D[keyof D], + N extends StringKey<D> = StringKey<D> +> extends ShellApiWithMongoClass { + _mongo: Mongo<M>; + _database: DatabaseWithSchema<M, D>; + _name: N; + + constructor( + mongo: Mongo<M>, + database: DatabaseWithSchema<M, D> | Database<M, D>, + name: N + ) { super(); this._mongo = mongo; - this._database = database; + this._database = database as DatabaseWithSchema<M, D>; this._name = name; const proxy = new Proxy(this, { get: (target, prop): any => { @@ -536,7 +563,7 @@ export default class Collection extends ShellApiWithMongoClass { query: Document = {}, projection?: Document, options: FindOptions = {} - ): Promise<Document | null> { + ): Promise<C['schema'] | null> { if (projection) { options.projection = projection; } @@ -1417,10 +1444,10 @@ export default class Collection extends ShellApiWithMongoClass { /** * Returns the collection database. * - * @return {Database} + * @return {DatabaseWithSchema} */ - @returnType('Database') - getDB(): Database { + @returnType('DatabaseWithSchema') + getDB(): DatabaseWithSchema<M, D> { this._emitCollectionApiCall('getDB'); return this._database; } @@ -1431,7 +1458,7 @@ export default class Collection extends ShellApiWithMongoClass { * @return {Mongo} */ @returnType('Mongo') - getMongo(): Mongo { + getMongo(): Mongo<M> { 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 @@ -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<M>); if (collStats[0].shard) { result.shards = shardStats; } @@ -2077,7 +2104,7 @@ export default class Collection extends ShellApiWithMongoClass { * @returns collection info based on given collStats. */ async _getShardedCollectionInfo( - config: Database, + config: DatabaseWithSchema<M, D>, collStats: Document[] ): Promise<Document> { const ns = `${this._database._name}.${this._name}`; @@ -2135,10 +2162,11 @@ 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; + // TODO: can we get around casting here? + const config = this._mongo.getDB( + 'config' as StringKey<M> + ) as DatabaseWithSchema<M, D>; const collStats = await ( await this.aggregate({ $collStats: { storageStats: {} } }) @@ -2248,7 +2276,10 @@ export default class Collection extends ShellApiWithMongoClass { } result.Totals = totalValue; - return new CommandResult<GetShardDistributionResult>('StatsResult', result); + return new CommandResult<GetShardDistributionResult>( + 'StatsResult', + result as GetShardDistributionResult + ); } @serverVersions(['3.1.0', ServerVersions.latest]) 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..366a858aeb 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 { Database } from './database'; +import { Collection } from './collection'; import Mongo from './mongo'; import type { AggregationCursor as ServiceProviderAggCursor, diff --git a/packages/shell-api/src/database.ts b/packages/shell-api/src/database.ts index c148cc14e1..ccdb61ec5a 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 { CollectionWithSchema } from './collection'; +import { Collection } 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,20 +74,33 @@ type AuthDoc = { mechanism?: string; }; +export type DatabaseWithSchema< + M extends GenericServerSideSchema = GenericServerSideSchema, + D extends GenericDatabaseSchema = GenericDatabaseSchema +> = Database<M, D> & { + [k in StringKey<D>]: Collection<M, D, D[k], k>; +}; + @shellApiClassDefault -export default class Database extends ShellApiWithMongoClass { - _mongo: Mongo; - _name: string; - _collections: Record<string, Collection>; +export class Database< + M extends GenericServerSideSchema = GenericServerSideSchema, + D extends GenericDatabaseSchema = GenericDatabaseSchema +> extends ShellApiWithMongoClass { + _mongo: Mongo<M>; + _name: StringKey<M>; + _collections: Record<StringKey<D>, CollectionWithSchema<M, D>>; _session: Session | undefined; - _cachedCollectionNames: string[] = []; + _cachedCollectionNames: StringKey<D>[] = []; _cachedHello: Document | null = null; - constructor(mongo: Mongo, name: string, session?: Session) { + constructor(mongo: Mongo<M>, name: StringKey<M>, session?: Session) { super(); this._mongo = mongo; this._name = name; - const collections: Record<string, Collection> = Object.create(null); + const collections: Record< + string, + CollectionWithSchema<M, D> + > = Object.create(null); this._collections = collections; this._session = session; const proxy = new Proxy(this, { @@ -99,7 +118,11 @@ export default class Database extends ShellApiWithMongoClass { } if (!collections[prop]) { - collections[prop] = new Collection(mongo, proxy, prop); + collections[prop] = new Collection<M, D>( + mongo, + proxy, + prop + ) as CollectionWithSchema<M, D>; } return collections[prop]; @@ -315,11 +338,11 @@ export default class Database extends ShellApiWithMongoClass { } @returnType('Mongo') - getMongo(): Mongo { + getMongo(): Mongo<M> { return this._mongo; } - getName(): string { + getName(): StringKey<M> { return this._name; } @@ -330,9 +353,9 @@ export default class Database extends ShellApiWithMongoClass { */ @returnsPromise @apiVersions([1]) - async getCollectionNames(): Promise<string[]> { + async getCollectionNames(): Promise<StringKey<D>[]> { this._emitDatabaseApiCall('getCollectionNames'); - return this._getCollectionNames(); + return (await this._getCollectionNames()) as StringKey<D>[]; } /** @@ -473,17 +496,19 @@ export default class Database extends ShellApiWithMongoClass { } @returnType('Database') - getSiblingDB(db: string): Database { + getSiblingDB<K extends StringKey<M>>(db: K): DatabaseWithSchema<M, M[K]> { assertArgsDefinedType([db], ['string'], 'Database.getSiblingDB'); this._emitDatabaseApiCall('getSiblingDB', { db }); if (this._session) { - return this._session.getDatabase(db); + return this._session.getDatabase(db) as DatabaseWithSchema<M, M[K]>; } return this._mongo._getDb(db); } @returnType('Collection') - getCollection(coll: string): Collection { + getCollection<K extends StringKey<D>>( + coll: K + ): CollectionWithSchema<M, D, D[K], K> { assertArgsDefinedType([coll], ['string'], 'Database.getColl'); this._emitDatabaseApiCall('getCollection', { coll }); if (!isValidCollectionName(coll)) { @@ -493,13 +518,18 @@ export default class Database extends ShellApiWithMongoClass { ); } - const collections: Record<string, Collection> = this._collections; + const collections: Record<string, CollectionWithSchema<M, D>> = this + ._collections; if (!collections[coll]) { - collections[coll] = new Collection(this._mongo, this, coll); + collections[coll] = new Collection<M, D>( + this._mongo, + this, + coll + ) as CollectionWithSchema<M, D>; } - return collections[coll]; + return collections[coll] as CollectionWithSchema<M, D, D[K], K>; } @returnsPromise 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<T extends { prototype: any }>( 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..4993e0454f 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 { Database } from './database'; import type Cursor from './cursor'; import Mongo from './mongo'; -import Collection from './collection'; +import { Collection } from './collection'; import Explainable from './explainable'; import type { ServiceProvider, Document } from '@mongosh/service-provider-core'; import { bson } from '@mongosh/service-provider-core'; diff --git a/packages/shell-api/src/explainable.ts b/packages/shell-api/src/explainable.ts index 393840af81..875493267e 100644 --- a/packages/shell-api/src/explainable.ts +++ b/packages/shell-api/src/explainable.ts @@ -1,4 +1,4 @@ -import type Collection from './collection'; +import type { CollectionWithSchema } from './collection'; import type Mongo from './mongo'; import ExplainableCursor from './explainable-cursor'; import { @@ -37,11 +37,11 @@ import type { @shellApiClassDefault export default class Explainable extends ShellApiWithMongoClass { _mongo: Mongo; - _collection: Collection; + _collection: CollectionWithSchema; _verbosity: ExplainVerbosityLike; constructor( mongo: Mongo, - collection: Collection, + collection: CollectionWithSchema, verbosity: ExplainVerbosityLike ) { super(); @@ -77,7 +77,7 @@ export default class Explainable extends ShellApiWithMongoClass { }); } - getCollection(): Collection { + getCollection(): CollectionWithSchema { this._emitExplainableApiCall('getCollection'); return this._collection; } diff --git a/packages/shell-api/src/field-level-encryption.spec.ts b/packages/shell-api/src/field-level-encryption.spec.ts index 9fab4b6246..2bf80279e6 100644 --- a/packages/shell-api/src/field-level-encryption.spec.ts +++ b/packages/shell-api/src/field-level-encryption.spec.ts @@ -16,7 +16,7 @@ import { Duplex } from 'stream'; import sinon from 'sinon'; import type { StubbedInstance } from 'ts-sinon'; import { stubInterface } from 'ts-sinon'; -import type Database from './database'; +import type { Database } from './database'; import { signatures, toShellResult } from './decorators'; import { ALL_PLATFORMS, @@ -34,7 +34,7 @@ import { makeFakeHTTPConnection, fakeAWSHandlers, } from '../../../testing/fake-kms'; -import Collection from './collection'; +import { Collection } from './collection'; import { dummyOptions } from './helpers.spec'; import type { IncomingMessage } from 'http'; @@ -112,7 +112,7 @@ describe('Field Level Encryption', function () { sp.createClientEncryption?.returns(libmongoc); sp.initialDb = 'test'; instanceState = new ShellInstanceState(sp, stubInterface<EventEmitter>()); - instanceState.currentDb = stubInterface<Database>(); + instanceState.currentDb = stubInterface<Database>() as any; mongo = new Mongo( instanceState, 'localhost:27017', @@ -195,7 +195,7 @@ describe('Field Level Encryption', function () { }); sp.initialDb = 'test'; instanceState = new ShellInstanceState(sp, stubInterface<EventEmitter>()); - instanceState.currentDb = stubInterface<Database>(); + instanceState.currentDb = stubInterface<Database>() as any; mongo = new Mongo( instanceState, 'localhost:27017', @@ -617,7 +617,7 @@ describe('Field Level Encryption', function () { sp.createClientEncryption?.returns(libmongoc); sp.initialDb = 'test'; instanceState = new ShellInstanceState(sp, stubInterface<EventEmitter>()); - instanceState.currentDb = stubInterface<Database>(); + instanceState.currentDb = stubInterface<Database>() as any; }); it('accepts the same local key twice', function () { const localKmsOptions: ClientSideFieldLevelEncryptionOptions = { @@ -709,7 +709,7 @@ describe('Field Level Encryption', function () { sp.createClientEncryption?.returns(libmongoc); sp.initialDb = 'test'; instanceState = new ShellInstanceState(sp, stubInterface<EventEmitter>()); - instanceState.currentDb = stubInterface<Database>(); + instanceState.currentDb = stubInterface<Database>() as any; }); it('fails to construct when FLE options are missing on Mongo', function () { mongo = new Mongo( diff --git a/packages/shell-api/src/field-level-encryption.ts b/packages/shell-api/src/field-level-encryption.ts index 486ca41c05..e4408b2a2f 100644 --- a/packages/shell-api/src/field-level-encryption.ts +++ b/packages/shell-api/src/field-level-encryption.ts @@ -19,7 +19,7 @@ import type { GCPEncryptionKeyOptions, } from '@mongosh/service-provider-core'; import type { Document, BinaryType } from '@mongosh/service-provider-core'; -import type Collection from './collection'; +import type { CollectionWithSchema } from './collection'; import Cursor from './cursor'; import type { DeleteResult } from './result'; import { assertArgsDefinedType, assertKeysDefined } from './helpers'; @@ -173,7 +173,7 @@ export class ClientEncryption extends ShellApiWithMongoClass { dbName: string, collName: string, options: CreateEncryptedCollectionOptions - ): Promise<{ collection: Collection; encryptedFields: Document }> { + ): Promise<{ collection: CollectionWithSchema; encryptedFields: Document }> { assertArgsDefinedType( [dbName], ['string'], @@ -217,7 +217,7 @@ export class ClientEncryption extends ShellApiWithMongoClass { export class KeyVault extends ShellApiWithMongoClass { public _mongo: Mongo; public _clientEncryption: ClientEncryption; - private _keyColl: Collection; + private _keyColl: CollectionWithSchema; constructor(clientEncryption: ClientEncryption) { super(); this._mongo = clientEncryption._mongo; diff --git a/packages/shell-api/src/helpers.ts b/packages/shell-api/src/helpers.ts index 69ec523e08..97d1ad4370 100644 --- a/packages/shell-api/src/helpers.ts +++ b/packages/shell-api/src/helpers.ts @@ -16,8 +16,8 @@ import { MongoshUnimplementedError, } from '@mongosh/errors'; import crypto from 'crypto'; -import type Database from './database'; -import type Collection from './collection'; +import type { Database } from './database'; +import type { Collection } from './collection'; import type { CursorIterationResult } from './result'; import { ShellApiErrors } from './error-codes'; import type { @@ -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<T> = 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..719178460d 100644 --- a/packages/shell-api/src/index.ts +++ b/packages/shell-api/src/index.ts @@ -1,8 +1,12 @@ import AggregationCursor from './aggregation-cursor'; import RunCommandCursor from './run-command-cursor'; -import Collection from './collection'; +import { CollectionWithSchema, Collection } from './collection'; import Cursor from './cursor'; -import Database, { CollectionNamesWithTypes } from './database'; +import { + Database, + DatabaseWithSchema, + CollectionNamesWithTypes, +} from './database'; import Explainable from './explainable'; import ExplainableCursor from './explainable-cursor'; import Help, { HelpProperties } from './help'; @@ -42,7 +46,9 @@ export { Cursor, CursorIterationResult, Database, + DatabaseWithSchema, Collection, + CollectionWithSchema, Explainable, ExplainableCursor, Help, diff --git a/packages/shell-api/src/integration.spec.ts b/packages/shell-api/src/integration.spec.ts index 58f15ce4e0..41eddfc286 100644 --- a/packages/shell-api/src/integration.spec.ts +++ b/packages/shell-api/src/integration.spec.ts @@ -3,8 +3,8 @@ import { NodeDriverServiceProvider } from '../../service-provider-node-driver'; import ShellInstanceState from './shell-instance-state'; import type Cursor from './cursor'; import Explainable from './explainable'; -import type Database from './database'; -import type Collection from './collection'; +import type { DatabaseWithSchema } from './database'; +import type { CollectionWithSchema } from './collection'; import { skipIfServerVersion, skipIfApiStrict, @@ -69,7 +69,9 @@ describe('Shell API (integration)', function () { expect(collectionNames).to.not.include(collectionName); }; - const loadQueryCache = async (collection: Collection): Promise<any> => { + const loadQueryCache = async ( + collection: CollectionWithSchema + ): Promise<any> => { const res = await collection.insertMany([ { _id: 1, item: 'abc', price: 12, quantity: 2, type: 'apparel' }, { _id: 2, item: 'jkl', price: 20, quantity: 1, type: 'electronics' }, @@ -103,7 +105,9 @@ describe('Shell API (integration)', function () { ).toArray(); }; - const loadMRExample = async (collection: Collection): Promise<any> => { + const loadMRExample = async ( + collection: CollectionWithSchema + ): Promise<any> => { const res = await collection.insertMany([ { _id: 1, @@ -225,8 +229,8 @@ describe('Shell API (integration)', function () { let shellApi: ShellApi; let mongo: Mongo; let dbName: string; - let database: Database; - let collection: Collection; + let database: DatabaseWithSchema; + let collection: CollectionWithSchema; let collectionName: string; beforeEach(async function () { diff --git a/packages/shell-api/src/interruptor.spec.ts b/packages/shell-api/src/interruptor.spec.ts index c378a28fe5..64d8554465 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 { Database } from './database'; import Mongo from './mongo'; import { InterruptFlag, MongoshInterruptedError } from './interruptor'; import ShellInstanceState from './shell-instance-state'; diff --git a/packages/shell-api/src/mongo-errors.spec.ts b/packages/shell-api/src/mongo-errors.spec.ts index 3b028f277d..9f0d8b63fc 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 { Database } from './database'; import type { EventEmitter } from 'events'; import ShellInstanceState from './shell-instance-state'; -import Collection from './collection'; +import { Collection } from './collection'; class MongoError extends Error { code?: number; diff --git a/packages/shell-api/src/mongo.spec.ts b/packages/shell-api/src/mongo.spec.ts index 303a57dfec..f8422220a0 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 { DatabaseWithSchema } from './database'; import { EventEmitter } from 'events'; import ShellInstanceState from './shell-instance-state'; -import Collection from './collection'; +import { Collection } 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<ServiceProvider>; - let database: StubbedInstance<Database>; + let database: StubbedInstance<DatabaseWithSchema>; let bus: StubbedInstance<EventEmitter>; let instanceState: ShellInstanceState; @@ -118,7 +118,7 @@ describe('Mongo', function () { undefined, serviceProvider ); - database = stubInterface<Database>(); + database = stubInterface<DatabaseWithSchema>(); instanceState.currentDb = database; }); describe('show', function () { @@ -940,14 +940,16 @@ describe('Mongo', function () { 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 collection 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 <db>.<collection>'); }); 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 <db>.<collection>'); }); diff --git a/packages/shell-api/src/mongo.ts b/packages/shell-api/src/mongo.ts index 193b91c0d8..efaf5ab047 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 { DatabaseWithSchema } from './database'; +import { Database } 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 { CollectionWithSchema } from './collection'; import type { ShellBson } from './shell-bson'; /* Utility, inverse of Readonly<T> */ @@ -73,16 +75,19 @@ type Mutable<T> = { @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<string, Database> = Object.create(null); + public readonly _databases: Record<StringKey<M>, DatabaseWithSchema<M>> = + 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<M>[] = []; constructor( instanceState: ShellInstanceState, @@ -252,7 +257,7 @@ export default class Mongo extends ShellApiClass { } } - _getDb(name: string): Database { + _getDb<K extends StringKey<M>>(name: K): DatabaseWithSchema<M, M[K]> { assertArgsDefinedType([name], ['string']); if (!isValidDatabaseName(name)) { throw new MongoshInvalidInputError( @@ -262,20 +267,25 @@ export default class Mongo extends ShellApiClass { } if (!(name in this._databases)) { - this._databases[name] = new Database(this, name); + this._databases[name] = new Database<M>(this, name) as DatabaseWithSchema< + M, + M[K] + >; } - return this._databases[name]; + return this._databases[name] as DatabaseWithSchema<M, M[K]>; } @returnType('Database') - getDB(db: string): Database { + getDB<K extends StringKey<M>>(db: K): DatabaseWithSchema<M, M[K]> { assertArgsDefinedType([db], ['string'], 'Mongo.getDB'); this._instanceState.messageBus.emit('mongosh:getDB', { db }); return this._getDb(db); } @returnType('Collection') - getCollection(name: string): Collection { + getCollection<KD extends StringKey<M>, KC extends StringKey<M[KD]>>( + name: `${KD}.${KC}` + ): CollectionWithSchema<M, M[KD], M[KD][KC]> { assertArgsDefinedType([name], ['string']); const { db, coll } = /^(?<db>[^.]+)\.(?<coll>.+)$/.exec(name)?.groups ?? {}; if (!db || !coll) { @@ -284,14 +294,16 @@ export default class Mongo extends ShellApiClass { CommonErrors.InvalidArgument ); } - return this._getDb(db).getCollection(coll); + return this._getDb(db as StringKey<M>).getCollection( + coll + ) as CollectionWithSchema<M, M[KD], M[KD][KC]>; } getURI(): string { return this._uri; } - use(db: string): string { + use(db: StringKey<M>): string { assertArgsDefinedType([db], ['string'], 'Mongo.use'); this._instanceState.messageBus.emit('mongosh:use', { db }); @@ -404,9 +416,13 @@ export default class Mongo extends ShellApiClass { @returnsPromise @apiVersions([1]) - async getDBNames(options: ListDatabasesOptions = {}): Promise<string[]> { + async getDBNames( + options: ListDatabasesOptions = {} + ): Promise<StringKey<M>[]> { 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<M> + ); } @returnsPromise @@ -883,17 +899,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<M>).aggregate([ + { $documents: [{}] }, + ...pipeline, + ]), + () => + this.getDB('admin' as StringKey<M>).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<M>) + .getCollection('system.version') + .aggregate(pipeline), // If that fails, try using $collStats for local.oplog.rs. () => - this.getDB('local') + this.getDB('local' as StringKey<M>) .getCollection('oplog.rs') .aggregate([{ $collStats: {} }, ...pipeline]), ]) { diff --git a/packages/shell-api/src/plan-cache.spec.ts b/packages/shell-api/src/plan-cache.spec.ts index aba263fb94..8efb5403b5 100644 --- a/packages/shell-api/src/plan-cache.spec.ts +++ b/packages/shell-api/src/plan-cache.spec.ts @@ -4,7 +4,7 @@ import { ALL_PLATFORMS, ALL_TOPOLOGIES, ServerVersions } from './enums'; import { signatures, toShellResult } from './index'; import type { StubbedInstance } from 'ts-sinon'; import { stubInterface } from 'ts-sinon'; -import type Collection from './collection'; +import type { CollectionWithSchema } from './collection'; import type AggregationCursor from './aggregation-cursor'; describe('PlanCache', function () { @@ -50,11 +50,11 @@ describe('PlanCache', function () { }); describe('commands', function () { let planCache: PlanCache; - let collection: StubbedInstance<Collection>; + let collection: StubbedInstance<CollectionWithSchema>; let aggCursor: StubbedInstance<AggregationCursor>; beforeEach(function () { - collection = stubInterface<Collection>(); + collection = stubInterface<CollectionWithSchema>(); planCache = new PlanCache(collection); }); describe('clear', function () { diff --git a/packages/shell-api/src/plan-cache.ts b/packages/shell-api/src/plan-cache.ts index 1be3022624..0da8453a62 100644 --- a/packages/shell-api/src/plan-cache.ts +++ b/packages/shell-api/src/plan-cache.ts @@ -7,16 +7,16 @@ import { ShellApiWithMongoClass, } from './decorators'; import type { Document } from '@mongosh/service-provider-core'; -import type Collection from './collection'; +import type { CollectionWithSchema } from './collection'; import { asPrintable, ServerVersions } from './enums'; import { MongoshDeprecatedError } from '@mongosh/errors'; import type Mongo from './mongo'; @shellApiClassDefault export default class PlanCache extends ShellApiWithMongoClass { - _collection: Collection; + _collection: CollectionWithSchema; - constructor(collection: Collection) { + constructor(collection: CollectionWithSchema) { super(); this._collection = collection; } diff --git a/packages/shell-api/src/replica-set.spec.ts b/packages/shell-api/src/replica-set.spec.ts index f03129b3c1..dbfdeb82cb 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 { Database } from './database'; import { ADMIN_DB, ALL_PLATFORMS, diff --git a/packages/shell-api/src/replica-set.ts b/packages/shell-api/src/replica-set.ts index cb0b0872df..064b502118 100644 --- a/packages/shell-api/src/replica-set.ts +++ b/packages/shell-api/src/replica-set.ts @@ -7,7 +7,7 @@ import { import { redactURICredentials } from '@mongosh/history'; import type { Document } from '@mongosh/service-provider-core'; import type Mongo from './mongo'; -import type Database from './database'; +import type { Database, DatabaseWithSchema } from './database'; import { deprecated, returnsPromise, @@ -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: DatabaseWithSchema<M, D>; - constructor(database: Database) { + constructor(database: DatabaseWithSchema<M, D> | Database<M, D>) { super(); - this._database = database; + this._database = database as DatabaseWithSchema<M, D>; } - get _mongo(): Mongo { + get _mongo(): Mongo<M> { return this._database._mongo; } diff --git a/packages/shell-api/src/session.spec.ts b/packages/shell-api/src/session.spec.ts index f3003caeb7..f44562d195 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 { Database } from './database'; import { CommonErrors, MongoshInvalidInputError } from '@mongosh/errors'; import { EventEmitter } from 'events'; import { dummyOptions } from './helpers.spec'; diff --git a/packages/shell-api/src/session.ts b/packages/shell-api/src/session.ts index cdae162d54..8619ae64d1 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 { DatabaseWithSchema } from './database'; +import { Database } 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<string, Database>; + public _mongo: Mongo<M>; + private _databases: Record<string, DatabaseWithSchema<M>>; constructor( - mongo: Mongo, + mongo: Mongo<M>, options: ClientSessionOptions, session: ClientSession ) { @@ -47,7 +51,7 @@ export default class Session extends ShellApiWithMongoClass { return this._session.id; } - getDatabase(name: string): Database { + getDatabase<K extends StringKey<M>>(name: K): DatabaseWithSchema<M, M[K]> { 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 Database<M>( + this._mongo, + name, + this + ) as DatabaseWithSchema<M, M[K]>; } - return this._databases[name]; + return this._databases[name] as DatabaseWithSchema<M, M[K]>; } advanceOperationTime(ts: TimestampType): void { diff --git a/packages/shell-api/src/shard.spec.ts b/packages/shell-api/src/shard.spec.ts index cfc6fd37fa..339e7544b9 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 { DatabaseWithSchema } from './database'; +import { Database } from './database'; import { inspect } from 'util'; import { dummyOptions } from './helpers.spec'; @@ -37,7 +38,7 @@ describe('Shard', function () { skipIfApiStrict(); describe('help', function () { - const apiClass: any = new Shard({} as any); + const apiClass: any = new Shard({} as DatabaseWithSchema); it('calls help function', async function () { expect((await toShellResult(apiClass.help())).type).to.equal('Help'); expect((await toShellResult(apiClass.help)).type).to.equal('Help'); @@ -76,7 +77,7 @@ describe('Shard', function () { describe('Metadata', function () { describe('toShellResult', function () { const mongo = { _uri: 'test_uri' } as Mongo; - const db = { _mongo: mongo, _name: 'test' } as Database; + const db = { _mongo: mongo, _name: 'test' } as DatabaseWithSchema; const sh = new Shard(db); it('value', async function () { expect((await toShellResult(sh)).printable).to.equal( @@ -2455,7 +2456,7 @@ describe('Shard', function () { const instanceState = new ShellInstanceState( apiStrictServiceProvider ); - const sh = new Shard(instanceState.currentDb); + const sh = new Shard(instanceState.currentDb as DatabaseWithSchema); const result = await sh.status(); expect(result.type).to.equal('StatsResult'); @@ -3439,7 +3440,7 @@ describe('Shard', function () { new EventEmitter() ); instanceState = new ShellInstanceState(serviceProvider); - sh = new Shard(instanceState.currentDb); + sh = new Shard(instanceState.currentDb as DatabaseWithSchema); // check replset uninitialized let members = await ( diff --git a/packages/shell-api/src/shard.ts b/packages/shell-api/src/shard.ts index be2a4e250e..67520e052a 100644 --- a/packages/shell-api/src/shard.ts +++ b/packages/shell-api/src/shard.ts @@ -1,4 +1,4 @@ -import type Database from './database'; +import type { Database, DatabaseWithSchema } from './database'; import { shellApiClassDefault, returnsPromise, @@ -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: DatabaseWithSchema<M, D>; - constructor(database: Database) { + constructor(database: DatabaseWithSchema<M, D> | Database<M, D>) { super(); - this._database = database; + this._database = database as DatabaseWithSchema<M, D>; } - get _mongo(): Mongo { + get _mongo(): Mongo<M> { return this._database._mongo; } @@ -205,7 +213,7 @@ export default class Shard extends ShellApiWithMongoClass { @apiVersions([1]) async status( verbose = false, - configDB?: Database + configDB?: DatabaseWithSchema<M, D> ): Promise<CommandResult<ShardingStatusResult>> { const result = await getPrintableShardStatus( configDB ?? (await getConfigDB(this._database)), diff --git a/packages/shell-api/src/shell-api.ts b/packages/shell-api/src/shell-api.ts index 1f553bd431..ffbadcf52b 100644 --- a/packages/shell-api/src/shell-api.ts +++ b/packages/shell-api/src/shell-api.ts @@ -14,7 +14,7 @@ import { } from './decorators'; import { asPrintable } from './enums'; import Mongo from './mongo'; -import type Database from './database'; +import type { DatabaseWithSchema } from './database'; import type { CommandResult } from './result'; import { CursorIterationResult } from './result'; import type ShellInstanceState from './shell-instance-state'; @@ -266,7 +266,11 @@ export default class ShellApi extends ShellApiClass { @returnsPromise @returnType('Database') @platforms(['CLI']) - async connect(uri: string, user?: string, pwd?: string): Promise<Database> { + async connect( + uri: string, + user?: string, + pwd?: string + ): Promise<DatabaseWithSchema> { assertArgsDefinedType( [uri, user, pwd], ['string', [undefined, 'string'], [undefined, 'string']], diff --git a/packages/shell-api/src/shell-instance-state.ts b/packages/shell-api/src/shell-instance-state.ts index 0ce2d93fc7..4d34899899 100644 --- a/packages/shell-api/src/shell-instance-state.ts +++ b/packages/shell-api/src/shell-instance-state.ts @@ -25,8 +25,8 @@ import type { AggregationCursor, Cursor, RunCommandCursor, - Database, ShellResult, + DatabaseWithSchema, } from './index'; import { getShellApiType, Mongo, ReplicaSet, Shard, ShellApi } from './index'; import { InterruptFlag } from './interruptor'; @@ -142,14 +142,14 @@ 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 | ChangeStreamCursor | RunCommandCursor | null; - public currentDb: Database; + public currentDb: DatabaseWithSchema; public messageBus: MongoshBus; public initialServiceProvider: ServiceProvider; // the initial service provider private connectionInfoCache: { @@ -219,7 +219,7 @@ export default class ShellInstanceState { initialServiceProvider.initialDb || DEFAULT_DB ); } else { - this.currentDb = new NoDatabase() as Database; + this.currentDb = new NoDatabase() as DatabaseWithSchema; } this.currentCursor = null; this.context = {}; @@ -282,7 +282,7 @@ export default class ShellInstanceState { this.preFetchCollectionAndDatabaseNames = value; } - public setDbFunc(newDb: any): Database { + public setDbFunc(newDb: any): DatabaseWithSchema { this.currentDb = newDb; this.context.rs = new ReplicaSet(this.currentDb); this.context.sh = new Shard(this.currentDb); @@ -351,7 +351,7 @@ export default class ShellInstanceState { contextObject.sh = new Shard(this.currentDb); contextObject.sp = Streams.newInstance(this.currentDb); - const setFunc = (newDb: any): Database => { + const setFunc = (newDb: any): DatabaseWithSchema => { if (getShellApiType(newDb) !== 'Database') { throw new MongoshInvalidInputError( "Cannot reassign 'db' to non-Database type", @@ -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..538045b10f 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 { Database } 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 Database(mongo, 'testDb'); + streams = new Streams(db); }); describe('createStreamProcessor', function () { diff --git a/packages/shell-api/src/streams.ts b/packages/shell-api/src/streams.ts index 127677148b..c0963521b5 100644 --- a/packages/shell-api/src/streams.ts +++ b/packages/shell-api/src/streams.ts @@ -8,13 +8,20 @@ import { } from './decorators'; import StreamProcessor from './stream-processor'; import { ADMIN_DB, asPrintable, shellApiType } from './enums'; -import type Database from './database'; +import type { Database, DatabaseWithSchema } 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: DatabaseWithSchema<M, D>) { + return new Proxy(new Streams<M, D>(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: DatabaseWithSchema<M, D>; - constructor(database: Database) { + constructor(database: DatabaseWithSchema<M, D> | Database<M, D>) { super(); - this._database = database; + this._database = database as DatabaseWithSchema<M, D>; } - get _mongo(): Mongo { + get _mongo(): Mongo<M> { 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); }