diff --git a/configs/tsconfig-mongosh/tsconfig.common.json b/configs/tsconfig-mongosh/tsconfig.common.json index ecca337371..b1e364d62e 100644 --- a/configs/tsconfig-mongosh/tsconfig.common.json +++ b/configs/tsconfig-mongosh/tsconfig.common.json @@ -14,7 +14,7 @@ "removeComments": true, "target": "es2018", "lib": ["es2019"], - "module": "commonjs", - "moduleResolution": "node" + "module": "nodenext", + "moduleResolution": "nodenext" } } diff --git a/package-lock.json b/package-lock.json index 3740344715..d790595413 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7107,17 +7107,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/@mongodb-js/dl-center": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/dl-center/-/dl-center-1.3.0.tgz", - "integrity": "sha512-5fsbPhmok5uyTdr3G3wf8YUWJm/TQnBHBeqRQV4CsSW15MguAv8YEx8cF8YXB20G01izkHT72xkqb/Ry4SiHcg==", - "license": "Apache-2.0", - "dependencies": { - "ajv": "^6.12.5", - "aws-sdk": "^2.1441.0", - "node-fetch": "^2.6.7" - } - }, "node_modules/@mongodb-js/eslint-config-devtools": { "version": "0.9.9", "resolved": "https://registry.npmjs.org/@mongodb-js/eslint-config-devtools/-/eslint-config-devtools-0.9.9.tgz", @@ -13346,9 +13335,9 @@ } }, "node_modules/@types/yargs-parser": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", - "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true, "license": "MIT" }, @@ -36700,9 +36689,9 @@ } }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -36715,20 +36704,55 @@ "dependencies": { "@mongosh/errors": "2.4.4", "@mongosh/i18n": "^2.19.0", - "mongodb-connection-string-url": "^3.0.2" + "mongodb-connection-string-url": "^3.0.2", + "yargs-parser": "^20.2.4" }, "devDependencies": { "@mongodb-js/devtools-connect": "^3.9.4", "@mongodb-js/eslint-config-mongosh": "^1.0.0", "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-mongosh": "^1.0.0", + "@types/yargs-parser": "^21.0.3", "depcheck": "^1.4.7", "eslint": "^7.25.0", "mongodb": "^6.19.0", - "prettier": "^2.8.8" + "prettier": "^2.8.8", + "strip-ansi": "^7.1.2" }, "engines": { "node": ">=14.15.1" + }, + "peerDependencies": { + "zod": "^3.25.76" + } + }, + "packages/arg-parser/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "packages/arg-parser/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "packages/async-rewriter2": { @@ -37432,7 +37456,7 @@ "license": "Apache-2.0", "dependencies": { "@mongodb-js/devtools-github-repo": "^1.4.2", - "@mongodb-js/dl-center": "^1.3.0", + "@mongodb-js/dl-center": "^1.4.4", "@mongodb-js/mongodb-downloader": "^0.3.7", "@mongodb-js/monorepo-tools": "^1.1.16", "@mongodb-js/signing-utils": "^0.3.7", @@ -37485,6 +37509,59 @@ "node": ">= 16" } }, + "packages/build/node_modules/@mongodb-js/dl-center": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/dl-center/-/dl-center-1.4.4.tgz", + "integrity": "sha512-xhJd0ja7Nf7HSUcYkXQSfgQUEFNdKaRd9NTjc09aMzXa2+OM5dA0AeBSiQyRP6DGVcgFHc52/J0mBG3FzM8kMA==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^6.12.5", + "aws-sdk": "^2.1441.0", + "node-fetch": "^2.7.0" + } + }, + "packages/build/node_modules/@mongodb-js/dl-center/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/build/node_modules/@mongodb-js/dl-center/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "packages/build/node_modules/@mongodb-js/dl-center/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "packages/build/node_modules/@mongodb-js/signing-utils": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/@mongodb-js/signing-utils/-/signing-utils-0.3.7.tgz", @@ -37790,8 +37867,7 @@ "pretty-repl": "^4.0.1", "semver": "^7.5.4", "strip-ansi": "^6.0.0", - "text-table": "^0.2.0", - "yargs-parser": "^20.2.4" + "text-table": "^0.2.0" }, "bin": { "mongosh": "bin/mongosh.js" @@ -37807,7 +37883,7 @@ "@types/node": "^22.15.30", "@types/numeral": "^2.0.2", "@types/text-table": "^0.2.1", - "@types/yargs-parser": "^15.0.0", + "@types/yargs-parser": "^21.0.3", "chai-as-promised": "^8.0.2", "depcheck": "^1.4.7", "eslint": "^7.25.0", @@ -38417,7 +38493,7 @@ "cross-spawn": "^7.0.5", "escape-string-regexp": "^4.0.0", "tar": "^6.1.15", - "zod": "^3.24.1" + "zod": "^3.25.76" }, "devDependencies": { "@mongodb-js/eslint-config-mongosh": "^1.0.0", diff --git a/packages/arg-parser/package.json b/packages/arg-parser/package.json index cce4f28c20..032c2326a9 100644 --- a/packages/arg-parser/package.json +++ b/packages/arg-parser/package.json @@ -3,6 +3,17 @@ "version": "3.22.2", "description": "MongoDB Shell CLI Argument List Parser Package", "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "exports": { + ".": { + "default": "./lib/index.js", + "types": "./lib/index.d.ts" + }, + "./arg-parser": { + "default": "./lib/arg-parser.js", + "types": "./lib/arg-parser.d.ts" + } + }, "repository": { "type": "git", "url": "git://github.com/mongodb-js/mongosh.git" @@ -37,16 +48,22 @@ "dependencies": { "@mongosh/errors": "2.4.4", "@mongosh/i18n": "^2.19.0", - "mongodb-connection-string-url": "^3.0.2" + "mongodb-connection-string-url": "^3.0.2", + "yargs-parser": "^20.2.4" + }, + "peerDependencies": { + "zod": "^3.25.76" }, "devDependencies": { "@mongodb-js/devtools-connect": "^3.9.4", "@mongodb-js/eslint-config-mongosh": "^1.0.0", "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-mongosh": "^1.0.0", + "@types/yargs-parser": "^21.0.3", "depcheck": "^1.4.7", "eslint": "^7.25.0", "mongodb": "^6.19.0", - "prettier": "^2.8.8" + "prettier": "^2.8.8", + "strip-ansi": "^7.1.2" } } diff --git a/packages/arg-parser/src/arg-parser.spec.ts b/packages/arg-parser/src/arg-parser.spec.ts new file mode 100644 index 0000000000..1923823b7f --- /dev/null +++ b/packages/arg-parser/src/arg-parser.spec.ts @@ -0,0 +1,1420 @@ +import { MongoshUnimplementedError } from '@mongosh/errors'; +import { expect } from 'chai'; +import stripAnsi from 'strip-ansi'; +import { + CliOptionsSchema, + coerceIfBoolean, + generateYargsOptionsFromSchema, + getLocale, + parseCliArgs, + parseMongoshCliOptionsArgs, + UnknownCliArgumentError, +} from './arg-parser'; +import { z } from 'zod/v4'; + +describe('arg-parser', function () { + describe('.getLocale', function () { + context('when --locale is provided', function () { + it('returns the locale', function () { + expect(getLocale(['--locale', 'de_DE'], {})).to.equal('de_DE'); + }); + }); + + context('when --locale is not provided', function () { + context('when env.LANG is set', function () { + context('when it contains the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LANG: 'de_DE.UTF-8' })).to.equal('de_DE'); + }); + }); + + context('when it does not contain the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LANG: 'de_DE' })).to.equal('de_DE'); + }); + }); + }); + + context('when env.LANGUAGE is set', function () { + context('when it contains the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LANGUAGE: 'de_DE.UTF-8' })).to.equal( + 'de_DE' + ); + }); + }); + + context('when it does not contain the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LANGUAGE: 'de_DE' })).to.equal('de_DE'); + }); + }); + }); + + context('when env.LC_ALL is set', function () { + context('when it contains the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LC_ALL: 'de_DE.UTF-8' })).to.equal('de_DE'); + }); + }); + + context('when it does not contain the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LC_ALL: 'de_DE' })).to.equal('de_DE'); + }); + }); + }); + + context('when env.LC_MESSAGES is set', function () { + context('when it contains the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LC_MESSAGES: 'de_DE.UTF-8' })).to.equal( + 'de_DE' + ); + }); + }); + + context('when it does not contain the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LC_MESSAGES: 'de_DE' })).to.equal('de_DE'); + }); + }); + }); + }); + }); + + describe('.parse', function () { + const baseArgv = ['node', 'mongosh']; + context('when providing only a URI', function () { + const uri = 'mongodb://domain.com:20000'; + const argv = [...baseArgv, uri]; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + }); + + context('when providing a URI + options', function () { + const uri = 'mongodb://domain.com:20000'; + + context('when providing general options', function () { + context('when providing --ipv6', function () { + const argv = [...baseArgv, uri, '--ipv6']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the ipv6 value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.ipv6).to.equal( + true + ); + }); + }); + + context('when providing -h', function () { + const argv = [...baseArgv, uri, '-h']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the help value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.help).to.equal( + true + ); + }); + }); + + context('when providing --help', function () { + const argv = [...baseArgv, uri, '--help']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the help value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.help).to.equal( + true + ); + }); + }); + + context('when providing --version', function () { + const argv = [...baseArgv, uri, '--version']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the version value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.version).to.equal( + true + ); + }); + }); + + context('when providing --verbose', function () { + const argv = [...baseArgv, uri, '--verbose']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the verbose value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.verbose).to.equal( + true + ); + }); + }); + + context('when providing --shell', function () { + const argv = [...baseArgv, uri, '--shell']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the shell value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.shell).to.equal( + true + ); + }); + }); + + context('when providing --nodb', function () { + const argv = [...baseArgv, uri, '--nodb']; + + it('does not return the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(undefined); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames + ).to.deep.equal([uri]); + }); + + it('sets the nodb value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.nodb).to.equal( + true + ); + }); + }); + + context('when providing --norc', function () { + const argv = [...baseArgv, uri, '--norc']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the norc value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.norc).to.equal( + true + ); + }); + }); + + context('when providing --quiet', function () { + const argv = [...baseArgv, uri, '--quiet']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the quiet value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.quiet).to.equal( + true + ); + }); + }); + + context('when providing --eval (single value)', function () { + const argv = [...baseArgv, uri, '--eval', '1+1']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the eval value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.eval).to.deep.equal( + ['1+1'] + ); + }); + }); + + context('when providing --eval (multiple values)', function () { + const argv = [...baseArgv, uri, '--eval', '1+1', '--eval', '2+2']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the eval value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.eval).to.deep.equal( + ['1+1', '2+2'] + ); + }); + }); + + context('when providing --retryWrites', function () { + const argv = [...baseArgv, uri, '--retryWrites']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the retryWrites value in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.retryWrites + ).to.equal(true); + }); + }); + + context('when providing an unknown parameter', function () { + const argv = [...baseArgv, uri, '--what']; + + it('raises an error', function () { + try { + parseMongoshCliOptionsArgs(argv).options; + } catch (err: any) { + if (err instanceof UnknownCliArgumentError) { + expect(stripAnsi(err.message)).to.equal( + 'Unknown argument: --what' + ); + return; + } + expect.fail('Expected UnknownCliArgumentError'); + } + expect.fail('parsing unknown parameter did not throw'); + }); + }); + }); + + context('when providing authentication options', function () { + context('when providing -u', function () { + const argv = [...baseArgv, uri, '-u', 'richard']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the username in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.username).to.equal( + 'richard' + ); + }); + }); + + context('when providing --username', function () { + const argv = [...baseArgv, uri, '--username', 'richard']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the username in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.username).to.equal( + 'richard' + ); + }); + }); + + context('when providing -p', function () { + const argv = [...baseArgv, uri, '-p', 'pw']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the password in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.password).to.equal( + 'pw' + ); + }); + }); + + context('when providing --password', function () { + const argv = [...baseArgv, uri, '--password', 'pw']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the password in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.password).to.equal( + 'pw' + ); + }); + }); + + context('when providing --authenticationDatabase', function () { + const argv = [...baseArgv, uri, '--authenticationDatabase', 'db']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the authenticationDatabase in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.authenticationDatabase + ).to.equal('db'); + }); + }); + + context('when providing --authenticationMechanism', function () { + const argv = [ + ...baseArgv, + uri, + '--authenticationMechanism', + 'SCRAM-SHA-256', + ]; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the authenticationMechanism in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.authenticationMechanism + ).to.equal('SCRAM-SHA-256'); + }); + }); + + context('when providing --gssapiServiceName', function () { + const argv = [...baseArgv, uri, '--gssapiServiceName', 'mongosh']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the gssapiServiceName in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.gssapiServiceName + ).to.equal('mongosh'); + }); + }); + + context('when providing --gssapiHostName', function () { + const argv = [...baseArgv, uri, '--gssapiHostName', 'example.com']; + + it('throws an error since it is not supported', function () { + try { + parseMongoshCliOptionsArgs(argv).options; + } catch (e: any) { + expect(e).to.be.instanceOf(MongoshUnimplementedError); + expect(e.message).to.include( + 'Argument --gssapiHostName is not supported in mongosh' + ); + return; + } + expect.fail('Expected error'); + }); + + // it('returns the URI in the object', () => { + // expect(parseMongoshCliOptionsArgs(argv).options.connectionSpecifier).to.equal(uri); + // }); + + // it('sets the gssapiHostName in the object', () => { + // expect(parseMongoshCliOptionsArgs(argv).options.gssapiHostName).to.equal('example.com'); + // }); + }); + + context('when providing --sspiHostnameCanonicalization', function () { + const argv = [ + ...baseArgv, + uri, + '--sspiHostnameCanonicalization', + 'forward', + ]; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the gssapiHostName in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options + .sspiHostnameCanonicalization + ).to.equal('forward'); + }); + }); + + context('when providing --sspiRealmOverride', function () { + const argv = [ + ...baseArgv, + uri, + '--sspiRealmOverride', + 'example2.com', + ]; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the gssapiHostName in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.sspiRealmOverride + ).to.equal('example2.com'); + }); + }); + + context('when providing --awsIamSessionToken', function () { + const argv = [...baseArgv, uri, '--awsIamSessionToken', 'tok']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the awsIamSessionToken in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.awsIamSessionToken + ).to.equal('tok'); + }); + }); + }); + + context('when providing TLS options', function () { + context('when providing --tls', function () { + const argv = [...baseArgv, uri, '--tls']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the tls in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.tls).to.equal(true); + }); + }); + + context('when providing -tls (single dash)', function () { + const argv = [...baseArgv, uri, '-tls']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the tls in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.tls).to.equal(true); + }); + }); + + context('when providing --tlsCertificateKeyFile', function () { + const argv = [...baseArgv, uri, '--tlsCertificateKeyFile', 'test']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the tlsCertificateKeyFile in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.tlsCertificateKeyFile + ).to.equal('test'); + }); + }); + + context( + 'when providing -tlsCertificateKeyFile (single dash)', + function () { + const argv = [...baseArgv, uri, '-tlsCertificateKeyFile', 'test']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the tlsCertificateKeyFile in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.tlsCertificateKeyFile + ).to.equal('test'); + }); + } + ); + + context('when providing --tlsCertificateKeyFilePassword', function () { + const argv = [ + ...baseArgv, + uri, + '--tlsCertificateKeyFilePassword', + 'test', + ]; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the tlsCertificateKeyFilePassword in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options + .tlsCertificateKeyFilePassword + ).to.equal('test'); + }); + }); + + context('when providing --tlsCAFile', function () { + const argv = [...baseArgv, uri, '--tlsCAFile', 'test']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the tlsCAFile in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.tlsCAFile).to.equal( + 'test' + ); + }); + }); + + context('when providing --tlsCRLFile', function () { + const argv = [...baseArgv, uri, '--tlsCRLFile', 'test']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the tlsCRLFile in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.tlsCRLFile + ).to.equal('test'); + }); + }); + + context('when providing --tlsAllowInvalidHostnames', function () { + const argv = [...baseArgv, uri, '--tlsAllowInvalidHostnames']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the tlsAllowInvalidHostnames in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.tlsAllowInvalidHostnames + ).to.equal(true); + }); + }); + + context('when providing --tlsAllowInvalidCertificates', function () { + const argv = [...baseArgv, uri, '--tlsAllowInvalidCertificates']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the tlsAllowInvalidCertificates in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options + .tlsAllowInvalidCertificates + ).to.equal(true); + }); + }); + + context('when providing --sslFIPSMode', function () { + const argv = [...baseArgv, uri, '--sslFIPSMode']; + + it('throws an error since it is not supported', function () { + try { + parseMongoshCliOptionsArgs(argv).options; + } catch (e: any) { + expect(e).to.be.instanceOf(MongoshUnimplementedError); + expect(e.message).to.include( + 'Argument --sslFIPSMode is not supported in mongosh' + ); + return; + } + expect.fail('Expected error'); + }); + + // it('returns the URI in the object', () => { + // expect(parseMongoshCliOptionsArgs(argv).options.connectionSpecifier).to.equal(uri); + // }); + + // it('sets the tlsFIPSMode in the object', () => { + // expect(parseMongoshCliOptionsArgs(argv).options.tlsFIPSMode).to.equal(true); + // }); + }); + + context('when providing --tlsCertificateSelector', function () { + const argv = [...baseArgv, uri, '--tlsCertificateSelector', 'test']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the tlsCertificateSelector in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.tlsCertificateSelector + ).to.equal('test'); + }); + }); + + context('when providing --tlsDisabledProtocols', function () { + const argv = [ + ...baseArgv, + uri, + '--tlsDisabledProtocols', + 'TLS1_0,TLS2_0', + ]; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the tlsDisabledProtocols in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.tlsDisabledProtocols + ).to.equal('TLS1_0,TLS2_0'); + }); + }); + }); + + context('when providing FLE options', function () { + context('when providing --awsAccessKeyId', function () { + const argv = [...baseArgv, uri, '--awsAccessKeyId', 'foo']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the awsAccessKeyId in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.awsAccessKeyId + ).to.equal('foo'); + }); + }); + + context('when providing --awsSecretAccessKey', function () { + const argv = [...baseArgv, uri, '--awsSecretAccessKey', 'foo']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the awsSecretAccessKey in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.awsSecretAccessKey + ).to.equal('foo'); + }); + }); + + context('when providing --awsSessionToken', function () { + const argv = [...baseArgv, uri, '--awsSessionToken', 'foo']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the awsSessionToken in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.awsSessionToken + ).to.equal('foo'); + }); + }); + + context('when providing --keyVaultNamespace', function () { + const argv = [...baseArgv, uri, '--keyVaultNamespace', 'foo.bar']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the keyVaultNamespace in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.keyVaultNamespace + ).to.equal('foo.bar'); + }); + }); + + context('when providing --kmsURL', function () { + const argv = [...baseArgv, uri, '--kmsURL', 'example.com']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the kmsURL in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.kmsURL).to.equal( + 'example.com' + ); + }); + }); + }); + + context('when providing versioned API options', function () { + context('when providing --apiVersion', function () { + const argv = [...baseArgv, uri, '--apiVersion', '1']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the apiVersion in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.apiVersion + ).to.equal('1'); + }); + }); + + context('when providing --apiDeprecationErrors', function () { + const argv = [...baseArgv, uri, '--apiDeprecationErrors']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the apiVersion in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.apiDeprecationErrors + ).to.equal(true); + }); + }); + + context('when providing --apiStrict', function () { + const argv = [...baseArgv, uri, '--apiStrict']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the apiVersion in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.apiStrict).to.equal( + true + ); + }); + }); + }); + + context('when providing filenames after an URI', function () { + context('when the filenames end in .js', function () { + const argv = [...baseArgv, uri, 'test1.js', 'test2.js']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the filenames', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.js'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.js'); + }); + }); + + context('when the filenames end in .mongodb', function () { + const argv = [...baseArgv, uri, 'test1.mongodb', 'test2.mongodb']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the filenames', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.mongodb'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.mongodb'); + }); + }); + + context('when the filenames end in other extensions', function () { + const argv = [...baseArgv, uri, 'test1.txt', 'test2.txt']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the filenames', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.txt'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.txt'); + }); + }); + + context('when filenames are specified using -f', function () { + const argv = [...baseArgv, uri, '-f', 'test1.txt', '-f', 'test2.txt']; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the filenames', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.txt'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.txt'); + }); + }); + + context('when filenames are specified using -f/--file', function () { + const argv = [ + ...baseArgv, + uri, + '-f', + 'test1.txt', + '--file', + 'test2.txt', + ]; + + it('returns the URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); + }); + + it('sets the filenames', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.txt'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.txt'); + }); + }); + }); + + context('when providing filenames without an URI', function () { + context('when the filenames end in .js', function () { + const argv = [...baseArgv, 'test1.js', 'test2.js']; + + it('returns no URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(undefined); + }); + + it('sets the filenames', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.js'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.js'); + }); + }); + + context('when the filenames end in .mongodb', function () { + const argv = [...baseArgv, 'test1.mongodb', 'test2.mongodb']; + + it('returns no URI in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(undefined); + }); + + it('sets the filenames', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.mongodb'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.mongodb'); + }); + }); + + context('when the filenames end in other extensions', function () { + const argv = [...baseArgv, 'test1.txt', 'test2.txt']; + + it('returns the first filename as an URI', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal('test1.txt'); + }); + + it('uses the remainder as filenames', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test2.txt'); + }); + }); + + context('when the first argument is an URI ending in .js', function () { + const argv = [...baseArgv, 'mongodb://domain.foo.js', 'test2.txt']; + + it('returns the first filename as an URI', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal('mongodb://domain.foo.js'); + }); + + it('uses the remainder as filenames', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test2.txt'); + }); + }); + + context( + 'when the first argument is an URI ending in .js but --file is used', + function () { + const argv = [ + ...baseArgv, + '--file', + 'mongodb://domain.foo.js', + 'mongodb://domain.bar.js', + ]; + + it('returns the first filename as an URI', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal('mongodb://domain.bar.js'); + }); + + it('uses the remainder as filenames', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('mongodb://domain.foo.js'); + }); + } + ); + }); + }); + + context('when providing no URI', function () { + context('when providing a DB address', function () { + context('when only a db name is provided', function () { + const db = 'foo'; + const argv = [...baseArgv, db]; + + it('sets the db in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(db); + }); + }); + + context('when a db address is provided without a scheme', function () { + const db = '192.168.0.5:9999/foo'; + const argv = [...baseArgv, db]; + + it('sets the db in the object', function () { + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(db); + }); + }); + }); + + context('when providing no DB address', function () { + context('when providing a host', function () { + const argv = [...baseArgv, '--host', 'example.com']; + + it('sets the host value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.host).to.equal( + 'example.com' + ); + }); + }); + + context('when providing a port', function () { + const argv = [...baseArgv, '--port', '20000']; + + it('sets the port value in the object', function () { + expect(parseMongoshCliOptionsArgs(argv).options.port).to.equal( + '20000' + ); + }); + }); + }); + }); + + context('when providing a deprecated argument', function () { + for (const { deprecated, replacement, value } of [ + { deprecated: 'ssl', replacement: 'tls' }, + { + deprecated: 'sslAllowInvalidCertificates', + replacement: 'tlsAllowInvalidCertificates', + }, + { + deprecated: 'sslAllowInvalidCertificates', + replacement: 'tlsAllowInvalidCertificates', + }, + { + deprecated: 'sslAllowInvalidHostnames', + replacement: 'tlsAllowInvalidHostnames', + }, + // { deprecated: 'sslFIPSMode', replacement: 'tlsFIPSMode' }, <<-- FIPS is currently not supported right now + { + deprecated: 'sslPEMKeyFile', + replacement: 'tlsCertificateKeyFile', + value: 'pemKeyFile', + }, + { + deprecated: 'sslPEMKeyPassword', + replacement: 'tlsCertificateKeyFilePassword', + value: 'pemKeyPass', + }, + { deprecated: 'sslCAFile', replacement: 'tlsCAFile', value: 'caFile' }, + // { deprecated: 'sslCertificateSelector', replacement: 'tlsCertificateSelector', value: 'certSelector' }, <<-- Certificate selector not supported right now + { + deprecated: 'sslCRLFile', + replacement: 'tlsCRLFile', + value: 'crlFile', + }, + { + deprecated: 'sslDisabledProtocols', + replacement: 'tlsDisabledProtocols', + value: 'disabledProtos', + }, + ] as const) { + it(`replaces --${deprecated} with --${replacement}`, function () { + const argv = [...baseArgv, `--${deprecated}`]; + if (value) { + argv.push(value); + } + + const args = parseMongoshCliOptionsArgs(argv).options; + expect(args).to.not.have.property(deprecated); + expect(args[replacement]).to.equal(value ?? true); + }); + } + }); + }); + + describe('union type fields', function () { + for (const { argument, values, strict } of [ + { argument: 'json', values: ['relaxed', 'canonical'] }, + { argument: 'oidcDumpTokens', values: ['redacted', 'include-secrets'] }, + { argument: 'browser', values: ['test'], strict: false }, + ] as const) { + const baseArgv = ['node', 'mongosh', 'mongodb://domain.com:20000']; + describe(`with ${argument}`, function () { + context('with boolean', function () { + it(`get set to true with --${argument}`, function () { + expect( + parseMongoshCliOptionsArgs([...baseArgv, `--${argument}`]) + .options[argument] + ).to.equal(true); + }); + + it(`sets to true with --${argument}=true`, function () { + expect( + parseMongoshCliOptionsArgs([...baseArgv, `--${argument}=true`]) + .options[argument] + ).to.equal(true); + }); + + it(`sets to false with --${argument}=false`, function () { + expect( + parseMongoshCliOptionsArgs([...baseArgv, `--${argument}=false`]) + .options[argument] + ).to.equal(false); + }); + }); + + for (const value of values) { + context('with string value', function () { + // This matches the legacy behavior pre-Zod schema migration. + it(`does not work with "--${argument} ${value}"`, function () { + expect( + parseMongoshCliOptionsArgs([ + ...baseArgv, + `--${argument} ${value}`, + ]).options[argument] + ).to.be.undefined; + }); + + it(`works "--${argument}=${value}"`, function () { + expect( + parseMongoshCliOptionsArgs([ + ...baseArgv, + `--${argument}=${value}`, + ]).options[argument] + ).to.equal(value); + }); + }); + } + + if (strict) { + it('throws an error with invalid value', function () { + try { + parseMongoshCliOptionsArgs([ + ...baseArgv, + `--${argument}`, + 'invalid', + ]); + } catch (e: any) { + expect(e).to.be.instanceOf(MongoshUnimplementedError); + expect(e.message).to.include( + `--${argument} can only have the values ${values.join(', ')}` + ); + return; + } + expect.fail('Expected error'); + }); + } + }); + } + }); + + const testSchema = z.object({ + name: z.string(), + age: z.number(), + isAdmin: z.boolean(), + roles: z.array(z.string()), + }); + + describe('generateYargsOptions', function () { + it('generates from arbitrary schema', function () { + const options = generateYargsOptionsFromSchema({ + schema: testSchema, + configuration: { + 'combine-arrays': true, + }, + }); + + expect(options).to.deep.equal({ + string: ['name'], + number: ['age'], + boolean: ['isAdmin'], + array: ['roles'], + coerce: {}, + alias: {}, + configuration: { + 'combine-arrays': true, + }, + }); + }); + + it('generates the expected options for Cli Options', function () { + const options = generateYargsOptionsFromSchema({ + schema: CliOptionsSchema, + }); + + const expected = { + string: [ + 'apiVersion', + 'authenticationDatabase', + 'authenticationMechanism', + 'awsAccessKeyId', + 'awsIamSessionToken', + 'awsSecretAccessKey', + 'awsSessionToken', + 'csfleLibraryPath', + 'cryptSharedLibPath', + 'db', + 'gssapiHostName', + 'gssapiServiceName', + 'sspiHostnameCanonicalization', + 'sspiRealmOverride', + 'jsContext', + 'host', + 'keyVaultNamespace', + 'kmsURL', + 'locale', + 'oidcFlows', + 'oidcRedirectUri', + 'password', + 'port', + 'sslPEMKeyFile', + 'sslPEMKeyPassword', + 'sslCAFile', + 'sslCertificateSelector', + 'sslCRLFile', + 'sslDisabledProtocols', + 'tlsCAFile', + 'tlsCertificateKeyFile', + 'tlsCertificateKeyFilePassword', + 'tlsCertificateSelector', + 'tlsCRLFile', + 'tlsDisabledProtocols', + 'username', + ], + boolean: [ + 'apiDeprecationErrors', + 'apiStrict', + 'buildInfo', + 'exposeAsyncRewriter', + 'help', + 'ipv6', + 'nodb', + 'norc', + 'oidcTrustedEndpoint', + 'oidcIdTokenAsAccessToken', + 'oidcNoNonce', + 'perfTests', + 'quiet', + 'retryWrites', + 'shell', + 'smokeTests', + 'skipStartupWarnings', + 'ssl', + 'sslAllowInvalidCertificates', + 'sslAllowInvalidHostnames', + 'sslFIPSMode', + 'tls', + 'tlsAllowInvalidCertificates', + 'tlsAllowInvalidHostnames', + 'tlsFIPSMode', + 'tlsUseSystemCA', + 'verbose', + 'version', + ], + array: ['eval', 'file'], + coerce: { + json: coerceIfBoolean, + oidcDumpTokens: coerceIfBoolean, + browser: coerceIfBoolean, + }, + alias: { + h: 'help', + p: 'password', + u: 'username', + f: 'file', + 'build-info': 'buildInfo', + oidcRedirectUrl: 'oidcRedirectUri', // I'd get this wrong about 50% of the time + oidcIDTokenAsAccessToken: 'oidcIdTokenAsAccessToken', // ditto + }, + configuration: { + 'camel-case-expansion': false, + 'unknown-options-as-args': true, + 'parse-positional-numbers': false, + 'parse-numbers': false, + 'greedy-arrays': false, + 'short-option-groups': false, + }, + }; + + // Compare arrays without caring about order + expect(options.string?.sort()).to.deep.equal(expected.string.sort()); + expect(options.boolean?.sort()).to.deep.equal(expected.boolean.sort()); + expect(options.array?.sort()).to.deep.equal(expected.array.sort()); + + // Compare non-array properties normally + expect(options.alias).to.deep.equal(expected.alias); + expect(options.configuration).to.deep.equal(expected.configuration); + }); + }); + + describe('parseCliArgs', function () { + it('parses the expected options for Cli Options', function () { + const options = parseCliArgs({ + args: ['--port', '20000', '--ssl', '1', '--unknownField', '1'], + schema: CliOptionsSchema, + }); + + expect(options).to.deep.equal({ + _: ['1', '--unknownField', '1'], + port: '20000', + ssl: true, + }); + }); + }); + + it('parses extended schema', function () { + const options = parseCliArgs({ + args: [ + '--port', + '20000', + '--extendedField', + '90', + '--unknownField', + '100', + ], + schema: CliOptionsSchema.extend({ + extendedField: z.number(), + }), + }); + + expect(options).to.deep.equal({ + _: ['--unknownField', '100'], + port: '20000', + extendedField: 90, + }); + }); +}); diff --git a/packages/arg-parser/src/arg-parser.ts b/packages/arg-parser/src/arg-parser.ts new file mode 100644 index 0000000000..5d1f237361 --- /dev/null +++ b/packages/arg-parser/src/arg-parser.ts @@ -0,0 +1,472 @@ +import { CommonErrors, MongoshUnimplementedError } from '@mongosh/errors'; +import i18n from '@mongosh/i18n'; +import type { CliOptions } from '@mongosh/arg-parser'; +import parser from 'yargs-parser'; +import { z } from 'zod/v4'; +import type { Options as YargsOptions } from 'yargs-parser'; + +/** + * Custom registry for CLI options metadata + */ +export const cliOptionsRegistry = z.registry(); + +/** + * CLI options schema with metadata attached via registry + */ +export const CliOptionsSchema = z + .object({ + // String options + apiVersion: z.string().optional(), + authenticationDatabase: z.string().optional(), + authenticationMechanism: z.string().optional(), + awsAccessKeyId: z.string().optional(), + awsIamSessionToken: z.string().optional(), + awsSecretAccessKey: z.string().optional(), + awsSessionToken: z.string().optional(), + csfleLibraryPath: z.string().optional(), + cryptSharedLibPath: z.string().optional(), + db: z.string().optional(), + gssapiHostName: z + .string() + .optional() + .register(cliOptionsRegistry, { unsupported: true }), + gssapiServiceName: z.string().optional(), + sspiHostnameCanonicalization: z.string().optional(), + sspiRealmOverride: z.string().optional(), + jsContext: z.string().optional(), + host: z.string().optional(), + keyVaultNamespace: z.string().optional(), + kmsURL: z.string().optional(), + locale: z.string().optional(), + oidcFlows: z.string().optional(), + oidcRedirectUri: z + .string() + .optional() + .register(cliOptionsRegistry, { + alias: ['oidcRedirectUrl'], + }), + password: z + .string() + .optional() + .register(cliOptionsRegistry, { alias: ['p'] }), + port: z.string().optional(), + username: z + .string() + .optional() + .register(cliOptionsRegistry, { alias: ['u'] }), + + // Deprecated SSL options (now TLS) + sslPEMKeyFile: z.string().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsCertificateKeyFile', + }), + sslPEMKeyPassword: z.string().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsCertificateKeyFilePassword', + }), + sslCAFile: z.string().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsCAFile', + }), + sslCertificateSelector: z.string().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsCertificateSelector', + }), + sslCRLFile: z.string().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsCRLFile', + }), + sslDisabledProtocols: z.string().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsDisabledProtocols', + }), + + // TLS options + tlsCAFile: z.string().optional(), + tlsCertificateKeyFile: z.string().optional(), + tlsCertificateKeyFilePassword: z.string().optional(), + tlsCertificateSelector: z.string().optional(), + tlsCRLFile: z.string().optional(), + tlsDisabledProtocols: z.string().optional(), + + // Boolean options + apiDeprecationErrors: z.boolean().optional(), + apiStrict: z.boolean().optional(), + buildInfo: z + .boolean() + .optional() + .register(cliOptionsRegistry, { alias: ['build-info'] }), + exposeAsyncRewriter: z.boolean().optional(), + help: z + .boolean() + .optional() + .register(cliOptionsRegistry, { alias: ['h'] }), + ipv6: z.boolean().optional(), + nodb: z.boolean().optional(), + norc: z.boolean().optional(), + oidcTrustedEndpoint: z.boolean().optional(), + oidcIdTokenAsAccessToken: z + .boolean() + .optional() + .register(cliOptionsRegistry, { + alias: ['oidcIDTokenAsAccessToken'], + }), + oidcNoNonce: z.boolean().optional(), + perfTests: z.boolean().optional(), + quiet: z.boolean().optional(), + retryWrites: z.boolean().optional(), + shell: z.boolean().optional(), + smokeTests: z.boolean().optional(), + skipStartupWarnings: z.boolean().optional(), + verbose: z.boolean().optional(), + version: z.boolean().optional(), + + // Deprecated SSL boolean options + ssl: z + .boolean() + .optional() + .register(cliOptionsRegistry, { deprecationReplacement: 'tls' }), + sslAllowInvalidCertificates: z + .boolean() + .optional() + .register(cliOptionsRegistry, { + deprecationReplacement: 'tlsAllowInvalidCertificates', + }), + sslAllowInvalidHostnames: z + .boolean() + .optional() + .register(cliOptionsRegistry, { + deprecationReplacement: 'tlsAllowInvalidHostnames', + }), + sslFIPSMode: z.boolean().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsFIPSMode', + unsupported: true, + }), + + // TLS boolean options + tls: z.boolean().optional(), + tlsAllowInvalidCertificates: z.boolean().optional(), + tlsAllowInvalidHostnames: z.boolean().optional(), + tlsFIPSMode: z.boolean().optional(), + tlsUseSystemCA: z.boolean().optional(), + + // Array options + eval: z.array(z.string()).optional(), + file: z + .array(z.string()) + .optional() + .register(cliOptionsRegistry, { alias: ['f'] }), + + // Options that can be boolean or string + json: z.union([z.boolean(), z.enum(['relaxed', 'canonical'])]).optional(), + oidcDumpTokens: z + .union([z.boolean(), z.enum(['redacted', 'include-secrets'])]) + .optional(), + browser: z.union([z.boolean(), z.string()]).optional(), + }) + .loose(); + +/** + * Metadata that can be used to define the yargs-parser configuration for a field. + */ +export type YargsOptionsMetadata = { + alias?: string[]; +}; + +/** + * Type for option metadata + */ +export type CliOptionsRegistryMetadata = YargsOptionsMetadata & { + deprecationReplacement?: keyof CliOptions; + unsupported?: boolean; +}; + +/** + * Extract metadata for a field using the custom registry + */ +const getCliOptionsMetadata = ( + fieldName: string +): CliOptionsRegistryMetadata | undefined => { + const fieldSchema = + CliOptionsSchema.shape[fieldName as keyof typeof CliOptionsSchema.shape]; + if (!fieldSchema) { + return undefined; + } + return cliOptionsRegistry.get(fieldSchema); +}; + +/** + * Generate yargs-parser configuration from schema + */ +export function generateYargsOptionsFromSchema({ + schema = CliOptionsSchema, + configuration = { + 'camel-case-expansion': false, + 'unknown-options-as-args': true, + 'parse-positional-numbers': false, + 'parse-numbers': false, + 'greedy-arrays': false, + 'short-option-groups': false, + }, +}: { + schema?: z.ZodObject; + configuration?: YargsOptions['configuration']; +}): YargsOptions { + const options = { + string: [], + boolean: [], + array: [], + alias: >{}, + coerce: unknown>>{}, + number: [], + } satisfies Required< + Pick< + YargsOptions, + 'string' | 'boolean' | 'array' | 'alias' | 'coerce' | 'number' + > + >; + + for (const [fieldName, fieldSchema] of Object.entries(schema.shape)) { + const meta = getCliOptionsMetadata(fieldName); + + // Unwrap optional type + let unwrappedType = fieldSchema; + if (fieldSchema instanceof z.ZodOptional) { + unwrappedType = fieldSchema.unwrap(); + } + + // Determine type + if (unwrappedType instanceof z.ZodArray) { + options.array.push(fieldName); + } else if (unwrappedType instanceof z.ZodBoolean) { + options.boolean.push(fieldName); + } else if (unwrappedType instanceof z.ZodString) { + options.string.push(fieldName); + } else if (unwrappedType instanceof z.ZodNumber) { + options.number.push(fieldName); + } else if (unwrappedType instanceof z.ZodUnion) { + // Handle union types (like json, browser, oidcDumpTokens) + // Check if the union includes boolean + const unionOptions = ( + unwrappedType as z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]> + ).options; + const hasBoolean = unionOptions.some( + (opt) => opt instanceof z.ZodBoolean + ); + const hasString = unionOptions.some( + (opt) => opt instanceof z.ZodString || opt instanceof z.ZodEnum + ); + + if (hasString && !hasBoolean) { + options.string.push(fieldName); + } + + if (hasBoolean && hasString) { + // When a field has both boolean and string, we add a coerce function to the field. + // This allows to get a value in both -- and --= for boolean and string. + options.coerce[fieldName] = coerceIfBoolean; + } + } else { + throw new Error(`Unknown field type: ${unwrappedType.constructor.name}`); + } + + // Add aliases + if (meta?.alias) { + for (const a of meta.alias) { + options.alias[a] = fieldName; + } + } + } + + return { + ...options, + configuration, + }; +} + +/** + * Maps deprecated arguments to their new counterparts, derived from schema metadata. + */ +function getDeprecatedArgsWithReplacement(): Record< + keyof z.infer, + keyof CliOptions +> { + const deprecated: Record = {}; + for (const fieldName of Object.keys(CliOptionsSchema.shape)) { + const meta = getCliOptionsMetadata(fieldName); + if (meta?.deprecationReplacement) { + deprecated[fieldName] = meta.deprecationReplacement; + } + } + return deprecated; +} + +/** + * Get list of unsupported arguments, derived from schema metadata. + */ +function getUnsupportedArgs(schema: z.ZodObject): string[] { + const unsupported: string[] = []; + for (const fieldName of Object.keys(schema.shape)) { + const meta = getCliOptionsMetadata(fieldName); + if (meta?.unsupported) { + unsupported.push(fieldName); + } + } + return unsupported; +} + +/** + * Determine the locale of the shell. + * + * @param {string[]} args - The arguments. + * + * @returns {string} The locale. + */ +export function getLocale(args: string[], env: any): string { + const localeIndex = args.indexOf('--locale'); + if (localeIndex > -1) { + return args[localeIndex + 1]; + } + const lang = env.LANG || env.LANGUAGE || env.LC_ALL || env.LC_MESSAGES; + return lang ? lang.split('.')[0] : lang; +} + +function isConnectionSpecifier(arg?: string): boolean { + return ( + typeof arg === 'string' && + (arg.startsWith('mongodb://') || + arg.startsWith('mongodb+srv://') || + !(arg.endsWith('.js') || arg.endsWith('.mongodb'))) + ); +} + +export function parseCliArgs({ + args, + schema, + parserConfiguration, +}: { + args: string[]; + schema?: z.ZodObject; + parserConfiguration?: YargsOptions['configuration']; +}): T & parser.Arguments { + const options = generateYargsOptionsFromSchema({ + schema, + configuration: parserConfiguration, + }); + + return parser(args, options) as unknown as T & parser.Arguments; +} + +/** + * Parses mongosh-specific arguments into a JS object. + * + * @param args - The CLI arguments. + * + * @returns The arguments as cli options. + */ +export function parseMongoshCliOptionsArgs(args: string[]): { + options: CliOptions; + warnings: string[]; +} { + const programArgs = args.slice(2); + i18n.setLocale(getLocale(programArgs, process.env)); + + const parsed = parseCliArgs< + CliOptions & { + smokeTests: boolean; + perfTests: boolean; + buildInfo: boolean; + file?: string[]; + } + >({ args: programArgs, schema: CliOptionsSchema }); + + const positionalArguments = parsed._ ?? []; + for (const arg of positionalArguments) { + if (typeof arg === 'string' && arg.startsWith('-')) { + throw new UnknownCliArgumentError(arg); + } + } + + if (typeof positionalArguments[0] === 'string') { + if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) { + parsed.connectionSpecifier = positionalArguments.shift() as string; + } + } + + // Remove the _ property from the parsed object + const { _: _exclude, ...parsedCliOptions } = parsed; + + return { + options: { + ...parsedCliOptions, + fileNames: [ + ...(parsedCliOptions.file ?? []), + ...(positionalArguments as string[]), + ], + }, + warnings: verifyCliArguments(parsed), + }; +} + +function verifyCliArguments(args: CliOptions): string[] { + const unsupportedArgs = getUnsupportedArgs(CliOptionsSchema); + for (const unsupported of unsupportedArgs) { + if (unsupported in args) { + throw new MongoshUnimplementedError( + `Argument --${unsupported} is not supported in mongosh`, + CommonErrors.InvalidArgument + ); + } + } + + const jsonValidation = CliOptionsSchema.shape.json.safeParse(args.json); + if (!jsonValidation.success) { + throw new MongoshUnimplementedError( + '--json can only have the values relaxed or canonical', + CommonErrors.InvalidArgument + ); + } + + const oidcDumpTokensValidation = + CliOptionsSchema.shape.oidcDumpTokens.safeParse(args.oidcDumpTokens); + if (!oidcDumpTokensValidation.success) { + throw new MongoshUnimplementedError( + '--oidcDumpTokens can only have the values redacted or include-secrets', + CommonErrors.InvalidArgument + ); + } + + const messages = []; + const deprecatedArgs = getDeprecatedArgsWithReplacement(); + for (const deprecated of Object.keys(deprecatedArgs)) { + if (deprecated in args) { + const replacement = deprecatedArgs[deprecated]; + messages.push( + `WARNING: argument --${deprecated} is deprecated and will be removed. Use --${replacement} instead.` + ); + + // This is a complicated type scenario. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (args as any)[replacement] = args[deprecated as keyof CliOptions]; + delete args[deprecated as keyof CliOptions]; + } + } + return messages; +} + +export function coerceIfBoolean(value: unknown) { + if (typeof value === 'string') { + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } + return value; + } + return value; +} + +export class UnknownCliArgumentError extends Error { + /** The argument that was not parsed. */ + readonly argument: string; + constructor(argument: string) { + super(`Unknown argument: ${argument}`); + this.name = 'UnknownCliArgumentError'; + this.argument = argument; + } +} diff --git a/packages/browser-repl/src/components/editor.tsx b/packages/browser-repl/src/components/editor.tsx index 4e3302c11c..614f99621e 100644 --- a/packages/browser-repl/src/components/editor.tsx +++ b/packages/browser-repl/src/components/editor.tsx @@ -67,7 +67,7 @@ export function createCommands( }, { key: 'ArrowUp', - run: (context) => { + run: (context: any) => { const selection = context.state.selection.main; if (!selection.empty) { return false; @@ -88,7 +88,7 @@ export function createCommands( }, { key: 'ArrowDown', - run: (context) => { + run: (context: any) => { const selection = context.state.selection.main; if (!selection.empty) { return false; @@ -146,7 +146,7 @@ export class Editor extends Component { constructor(props: EditorProps) { super(props); - this.autocompleter = (context) => { + this.autocompleter = (context: any) => { if (!this.props.autocompleter?.getCompletions) { return null; } diff --git a/packages/build/package.json b/packages/build/package.json index c75fe33701..c75f480165 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -65,7 +65,7 @@ }, "dependencies": { "@mongodb-js/devtools-github-repo": "^1.4.2", - "@mongodb-js/dl-center": "^1.3.0", + "@mongodb-js/dl-center": "^1.4.4", "@mongodb-js/mongodb-downloader": "^0.3.7", "@mongodb-js/monorepo-tools": "^1.1.16", "@mongodb-js/signing-utils": "^0.3.7", diff --git a/packages/build/src/download-center/config.spec.ts b/packages/build/src/download-center/config.spec.ts index 4b0285758a..43d70cca91 100644 --- a/packages/build/src/download-center/config.spec.ts +++ b/packages/build/src/download-center/config.spec.ts @@ -1,4 +1,4 @@ -import type { DownloadCenterConfig } from '@mongodb-js/dl-center/dist/download-center-config'; +import type { DownloadCenterConfig } from '@mongodb-js/dl-center'; import { type PackageInformationProvider } from '../packaging'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/packages/build/src/download-center/config.ts b/packages/build/src/download-center/config.ts index 9774e14040..fd6948c44d 100644 --- a/packages/build/src/download-center/config.ts +++ b/packages/build/src/download-center/config.ts @@ -6,7 +6,7 @@ import { major as majorVersion } from 'semver'; import type { DownloadCenterConfig, PlatformWithPackages, -} from '@mongodb-js/dl-center/dist/download-center-config'; +} from '@mongodb-js/dl-center'; import { ARTIFACTS_BUCKET, JSON_FEED_ARTIFACT_KEY, diff --git a/packages/cli-repl/package.json b/packages/cli-repl/package.json index 6745eae931..55df5e39f3 100644 --- a/packages/cli-repl/package.json +++ b/packages/cli-repl/package.json @@ -93,8 +93,7 @@ "semver": "^7.5.4", "strip-ansi": "^6.0.0", "text-table": "^0.2.0", - "glibc-version": "^1.0.0", - "yargs-parser": "^20.2.4" + "glibc-version": "^1.0.0" }, "devDependencies": { "@mongodb-js/eslint-config-mongosh": "^1.0.0", @@ -107,7 +106,7 @@ "@types/node": "^22.15.30", "@types/numeral": "^2.0.2", "@types/text-table": "^0.2.1", - "@types/yargs-parser": "^15.0.0", + "@types/yargs-parser": "^21.0.3", "chai-as-promised": "^8.0.2", "depcheck": "^1.4.7", "eslint": "^7.25.0", diff --git a/packages/cli-repl/src/arg-parser.spec.ts b/packages/cli-repl/src/arg-parser.spec.ts index ab09eff6ae..916dc64f16 100644 --- a/packages/cli-repl/src/arg-parser.spec.ts +++ b/packages/cli-repl/src/arg-parser.spec.ts @@ -1,946 +1,40 @@ -import { MongoshUnimplementedError } from '@mongosh/errors'; import { expect } from 'chai'; +import { parseMongoshCliArgs } from './arg-parser'; import stripAnsi from 'strip-ansi'; -import { getLocale, parseCliArgs } from './arg-parser'; -describe('arg-parser', function () { - describe('.getLocale', function () { - context('when --locale is provided', function () { - it('returns the locale', function () { - expect(getLocale(['--locale', 'de_DE'], {})).to.equal('de_DE'); - }); - }); - - context('when --locale is not provided', function () { - context('when env.LANG is set', function () { - context('when it contains the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LANG: 'de_DE.UTF-8' })).to.equal('de_DE'); - }); - }); - - context('when it does not contain the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LANG: 'de_DE' })).to.equal('de_DE'); - }); - }); - }); - - context('when env.LANGUAGE is set', function () { - context('when it contains the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LANGUAGE: 'de_DE.UTF-8' })).to.equal( - 'de_DE' - ); - }); - }); - - context('when it does not contain the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LANGUAGE: 'de_DE' })).to.equal('de_DE'); - }); - }); - }); - - context('when env.LC_ALL is set', function () { - context('when it contains the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LC_ALL: 'de_DE.UTF-8' })).to.equal('de_DE'); - }); - }); - - context('when it does not contain the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LC_ALL: 'de_DE' })).to.equal('de_DE'); - }); - }); - }); - - context('when env.LC_MESSAGES is set', function () { - context('when it contains the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LC_MESSAGES: 'de_DE.UTF-8' })).to.equal( - 'de_DE' - ); - }); - }); - - context('when it does not contain the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LC_MESSAGES: 'de_DE' })).to.equal('de_DE'); - }); - }); - }); - }); - }); - - describe('.parse', function () { - const baseArgv = ['node', 'mongosh']; - context('when providing only a URI', function () { - const uri = 'mongodb://domain.com:20000'; - const argv = [...baseArgv, uri]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - }); - - context('when providing a URI + options', function () { - const uri = 'mongodb://domain.com:20000'; - - context('when providing general options', function () { - context('when providing --ipv6', function () { - const argv = [...baseArgv, uri, '--ipv6']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the ipv6 value in the object', function () { - expect(parseCliArgs(argv).ipv6).to.equal(true); - }); - }); - - context('when providing -h', function () { - const argv = [...baseArgv, uri, '-h']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the help value in the object', function () { - expect(parseCliArgs(argv).help).to.equal(true); - }); - }); - - context('when providing --help', function () { - const argv = [...baseArgv, uri, '--help']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the help value in the object', function () { - expect(parseCliArgs(argv).help).to.equal(true); - }); - }); - - context('when providing --version', function () { - const argv = [...baseArgv, uri, '--version']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the version value in the object', function () { - expect(parseCliArgs(argv).version).to.equal(true); - }); - }); - - context('when providing --verbose', function () { - const argv = [...baseArgv, uri, '--verbose']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the verbose value in the object', function () { - expect(parseCliArgs(argv).verbose).to.equal(true); - }); - }); - - context('when providing --shell', function () { - const argv = [...baseArgv, uri, '--shell']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the shell value in the object', function () { - expect(parseCliArgs(argv).shell).to.equal(true); - }); - }); - - context('when providing --nodb', function () { - const argv = [...baseArgv, uri, '--nodb']; - - it('does not return the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(undefined); - expect(parseCliArgs(argv).fileNames).to.deep.equal([uri]); - }); - - it('sets the nodb value in the object', function () { - expect(parseCliArgs(argv).nodb).to.equal(true); - }); - }); - - context('when providing --norc', function () { - const argv = [...baseArgv, uri, '--norc']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the norc value in the object', function () { - expect(parseCliArgs(argv).norc).to.equal(true); - }); - }); - - context('when providing --quiet', function () { - const argv = [...baseArgv, uri, '--quiet']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the quiet value in the object', function () { - expect(parseCliArgs(argv).quiet).to.equal(true); - }); - }); - - context('when providing --eval (single value)', function () { - const argv = [...baseArgv, uri, '--eval', '1+1']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the eval value in the object', function () { - expect(parseCliArgs(argv).eval).to.deep.equal(['1+1']); - }); - }); - - context('when providing --eval (multiple values)', function () { - const argv = [...baseArgv, uri, '--eval', '1+1', '--eval', '2+2']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the eval value in the object', function () { - expect(parseCliArgs(argv).eval).to.deep.equal(['1+1', '2+2']); - }); - }); - - context('when providing --retryWrites', function () { - const argv = [...baseArgv, uri, '--retryWrites']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the retryWrites value in the object', function () { - expect(parseCliArgs(argv).retryWrites).to.equal(true); - }); - }); - - context('when providing an unknown parameter', function () { - const argv = [...baseArgv, uri, '--what']; - - it('raises an error', function () { - try { - parseCliArgs(argv); - } catch (err: any) { - return expect(stripAnsi(err.message)).to.contain( - 'Error parsing command line: unrecognized option: --what' - ); - } - expect.fail('parsing unknown parameter did not throw'); - }); - }); - }); - - context('when providing authentication options', function () { - context('when providing -u', function () { - const argv = [...baseArgv, uri, '-u', 'richard']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the username in the object', function () { - expect(parseCliArgs(argv).username).to.equal('richard'); - }); - }); - - context('when providing --username', function () { - const argv = [...baseArgv, uri, '--username', 'richard']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the username in the object', function () { - expect(parseCliArgs(argv).username).to.equal('richard'); - }); - }); - - context('when providing -p', function () { - const argv = [...baseArgv, uri, '-p', 'pw']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the password in the object', function () { - expect(parseCliArgs(argv).password).to.equal('pw'); - }); - }); - - context('when providing --password', function () { - const argv = [...baseArgv, uri, '--password', 'pw']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the password in the object', function () { - expect(parseCliArgs(argv).password).to.equal('pw'); - }); - }); - - context('when providing --authenticationDatabase', function () { - const argv = [...baseArgv, uri, '--authenticationDatabase', 'db']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the authenticationDatabase in the object', function () { - expect(parseCliArgs(argv).authenticationDatabase).to.equal('db'); - }); - }); - - context('when providing --authenticationMechanism', function () { - const argv = [ - ...baseArgv, - uri, - '--authenticationMechanism', - 'SCRAM-SHA-256', - ]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the authenticationMechanism in the object', function () { - expect(parseCliArgs(argv).authenticationMechanism).to.equal( - 'SCRAM-SHA-256' - ); - }); - }); - - context('when providing --gssapiServiceName', function () { - const argv = [...baseArgv, uri, '--gssapiServiceName', 'mongosh']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the gssapiServiceName in the object', function () { - expect(parseCliArgs(argv).gssapiServiceName).to.equal('mongosh'); - }); - }); - - context('when providing --gssapiHostName', function () { - const argv = [...baseArgv, uri, '--gssapiHostName', 'example.com']; - - it('throws an error since it is not supported', function () { - try { - parseCliArgs(argv); - } catch (e: any) { - expect(e).to.be.instanceOf(MongoshUnimplementedError); - expect(e.message).to.include( - 'Argument --gssapiHostName is not supported in mongosh' - ); - return; - } - expect.fail('Expected error'); - }); - - // it('returns the URI in the object', () => { - // expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - // }); - - // it('sets the gssapiHostName in the object', () => { - // expect(parseCliArgs(argv).gssapiHostName).to.equal('example.com'); - // }); - }); - - context('when providing --sspiHostnameCanonicalization', function () { - const argv = [ - ...baseArgv, - uri, - '--sspiHostnameCanonicalization', - 'forward', - ]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the gssapiHostName in the object', function () { - expect(parseCliArgs(argv).sspiHostnameCanonicalization).to.equal( - 'forward' - ); - }); - }); - - context('when providing --sspiRealmOverride', function () { - const argv = [ - ...baseArgv, - uri, - '--sspiRealmOverride', - 'example2.com', - ]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the gssapiHostName in the object', function () { - expect(parseCliArgs(argv).sspiRealmOverride).to.equal( - 'example2.com' - ); - }); - }); - - context('when providing --awsIamSessionToken', function () { - const argv = [...baseArgv, uri, '--awsIamSessionToken', 'tok']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the awsIamSessionToken in the object', function () { - expect(parseCliArgs(argv).awsIamSessionToken).to.equal('tok'); - }); - }); - }); - - context('when providing TLS options', function () { - context('when providing --tls', function () { - const argv = [...baseArgv, uri, '--tls']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tls in the object', function () { - expect(parseCliArgs(argv).tls).to.equal(true); - }); - }); - - context('when providing -tls (single dash)', function () { - const argv = [...baseArgv, uri, '-tls']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tls in the object', function () { - expect(parseCliArgs(argv).tls).to.equal(true); - }); - }); - - context('when providing --tlsCertificateKeyFile', function () { - const argv = [...baseArgv, uri, '--tlsCertificateKeyFile', 'test']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsCertificateKeyFile in the object', function () { - expect(parseCliArgs(argv).tlsCertificateKeyFile).to.equal('test'); - }); - }); - - context( - 'when providing -tlsCertificateKeyFile (single dash)', - function () { - const argv = [...baseArgv, uri, '-tlsCertificateKeyFile', 'test']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsCertificateKeyFile in the object', function () { - expect(parseCliArgs(argv).tlsCertificateKeyFile).to.equal('test'); - }); - } - ); - - context('when providing --tlsCertificateKeyFilePassword', function () { - const argv = [ - ...baseArgv, - uri, - '--tlsCertificateKeyFilePassword', - 'test', - ]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsCertificateKeyFilePassword in the object', function () { - expect(parseCliArgs(argv).tlsCertificateKeyFilePassword).to.equal( - 'test' - ); - }); - }); - - context('when providing --tlsCAFile', function () { - const argv = [...baseArgv, uri, '--tlsCAFile', 'test']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsCAFile in the object', function () { - expect(parseCliArgs(argv).tlsCAFile).to.equal('test'); - }); - }); - - context('when providing --tlsCRLFile', function () { - const argv = [...baseArgv, uri, '--tlsCRLFile', 'test']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsCRLFile in the object', function () { - expect(parseCliArgs(argv).tlsCRLFile).to.equal('test'); - }); - }); - - context('when providing --tlsAllowInvalidHostnames', function () { - const argv = [...baseArgv, uri, '--tlsAllowInvalidHostnames']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsAllowInvalidHostnames in the object', function () { - expect(parseCliArgs(argv).tlsAllowInvalidHostnames).to.equal(true); - }); - }); - - context('when providing --tlsAllowInvalidCertificates', function () { - const argv = [...baseArgv, uri, '--tlsAllowInvalidCertificates']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsAllowInvalidCertificates in the object', function () { - expect(parseCliArgs(argv).tlsAllowInvalidCertificates).to.equal( - true - ); - }); - }); - - context('when providing --sslFIPSMode', function () { - const argv = [...baseArgv, uri, '--sslFIPSMode']; - - it('throws an error since it is not supported', function () { - try { - parseCliArgs(argv); - } catch (e: any) { - expect(e).to.be.instanceOf(MongoshUnimplementedError); - expect(e.message).to.include( - 'Argument --sslFIPSMode is not supported in mongosh' - ); - return; - } - expect.fail('Expected error'); - }); - - // it('returns the URI in the object', () => { - // expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - // }); - - // it('sets the tlsFIPSMode in the object', () => { - // expect(parseCliArgs(argv).tlsFIPSMode).to.equal(true); - // }); - }); - - context('when providing --tlsCertificateSelector', function () { - const argv = [...baseArgv, uri, '--tlsCertificateSelector', 'test']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsCertificateSelector in the object', function () { - expect(parseCliArgs(argv).tlsCertificateSelector).to.equal('test'); - }); - }); - - context('when providing --tlsDisabledProtocols', function () { - const argv = [ - ...baseArgv, - uri, - '--tlsDisabledProtocols', - 'TLS1_0,TLS2_0', - ]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsDisabledProtocols in the object', function () { - expect(parseCliArgs(argv).tlsDisabledProtocols).to.equal( - 'TLS1_0,TLS2_0' - ); - }); - }); - }); - - context('when providing FLE options', function () { - context('when providing --awsAccessKeyId', function () { - const argv = [...baseArgv, uri, '--awsAccessKeyId', 'foo']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the awsAccessKeyId in the object', function () { - expect(parseCliArgs(argv).awsAccessKeyId).to.equal('foo'); - }); - }); - - context('when providing --awsSecretAccessKey', function () { - const argv = [...baseArgv, uri, '--awsSecretAccessKey', 'foo']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the awsSecretAccessKey in the object', function () { - expect(parseCliArgs(argv).awsSecretAccessKey).to.equal('foo'); - }); - }); - - context('when providing --awsSessionToken', function () { - const argv = [...baseArgv, uri, '--awsSessionToken', 'foo']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the awsSessionToken in the object', function () { - expect(parseCliArgs(argv).awsSessionToken).to.equal('foo'); - }); - }); - - context('when providing --keyVaultNamespace', function () { - const argv = [...baseArgv, uri, '--keyVaultNamespace', 'foo.bar']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the keyVaultNamespace in the object', function () { - expect(parseCliArgs(argv).keyVaultNamespace).to.equal('foo.bar'); - }); - }); - - context('when providing --kmsURL', function () { - const argv = [...baseArgv, uri, '--kmsURL', 'example.com']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the kmsURL in the object', function () { - expect(parseCliArgs(argv).kmsURL).to.equal('example.com'); - }); - }); - }); - - context('when providing versioned API options', function () { - context('when providing --apiVersion', function () { - const argv = [...baseArgv, uri, '--apiVersion', '1']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the apiVersion in the object', function () { - expect(parseCliArgs(argv).apiVersion).to.equal('1'); - }); - }); - - context('when providing --apiDeprecationErrors', function () { - const argv = [...baseArgv, uri, '--apiDeprecationErrors']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the apiVersion in the object', function () { - expect(parseCliArgs(argv).apiDeprecationErrors).to.equal(true); - }); - }); - - context('when providing --apiStrict', function () { - const argv = [...baseArgv, uri, '--apiStrict']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the apiVersion in the object', function () { - expect(parseCliArgs(argv).apiStrict).to.equal(true); - }); - }); - }); - - context('when providing filenames after an URI', function () { - context('when the filenames end in .js', function () { - const argv = [...baseArgv, uri, 'test1.js', 'test2.js']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.js'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.js'); - }); - }); - - context('when the filenames end in .mongodb', function () { - const argv = [...baseArgv, uri, 'test1.mongodb', 'test2.mongodb']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.mongodb'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.mongodb'); - }); - }); - - context('when the filenames end in other extensions', function () { - const argv = [...baseArgv, uri, 'test1.txt', 'test2.txt']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.txt'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.txt'); - }); - }); - - context('when filenames are specified using -f', function () { - const argv = [...baseArgv, uri, '-f', 'test1.txt', '-f', 'test2.txt']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.txt'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.txt'); - }); - }); - - context('when filenames are specified using -f/--file', function () { - const argv = [ - ...baseArgv, - uri, - '-f', - 'test1.txt', - '--file', - 'test2.txt', - ]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.txt'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.txt'); - }); - }); - }); - - context('when providing filenames without an URI', function () { - context('when the filenames end in .js', function () { - const argv = [...baseArgv, 'test1.js', 'test2.js']; - - it('returns no URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(undefined); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.js'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.js'); - }); - }); - - context('when the filenames end in .mongodb', function () { - const argv = [...baseArgv, 'test1.mongodb', 'test2.mongodb']; - - it('returns no URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(undefined); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.mongodb'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.mongodb'); - }); - }); - - context('when the filenames end in other extensions', function () { - const argv = [...baseArgv, 'test1.txt', 'test2.txt']; - - it('returns the first filename as an URI', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal( - 'test1.txt' - ); - }); - - it('uses the remainder as filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test2.txt'); - }); - }); - - context('when the first argument is an URI ending in .js', function () { - const argv = [...baseArgv, 'mongodb://domain.foo.js', 'test2.txt']; - - it('returns the first filename as an URI', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal( - 'mongodb://domain.foo.js' - ); - }); - - it('uses the remainder as filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test2.txt'); - }); - }); - - context( - 'when the first argument is an URI ending in .js but --file is used', - function () { - const argv = [ - ...baseArgv, - '--file', - 'mongodb://domain.foo.js', - 'mongodb://domain.bar.js', - ]; - - it('returns the first filename as an URI', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal( - 'mongodb://domain.bar.js' - ); - }); - - it('uses the remainder as filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal( - 'mongodb://domain.foo.js' - ); - }); - } +describe('parseMongoshCliArgs', function () { + const baseArgv = ['node', 'mongosh']; + const uri = 'mongodb://domain.com:2020'; + context('when providing an unknown parameter', function () { + const argv = [...baseArgv, uri, '--what']; + + it('raises an error', function () { + try { + parseMongoshCliArgs(argv); + } catch (err: any) { + return expect(stripAnsi(err.message)).to.contain( + 'Error parsing command line: unrecognized option: --what' ); - }); + } + expect.fail('parsing unknown parameter did not throw'); }); - context('when providing no URI', function () { - context('when providing a DB address', function () { - context('when only a db name is provided', function () { - const db = 'foo'; - const argv = [...baseArgv, db]; - - it('sets the db in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(db); - }); - }); - - context('when a db address is provided without a scheme', function () { - const db = '192.168.0.5:9999/foo'; - const argv = [...baseArgv, db]; + context('parses standard arguments correctly', function () { + it('sets passed fields', function () { + const argv = [...baseArgv, uri, '--tls', '--port', '1234']; - it('sets the db in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(db); - }); - }); + const args = parseMongoshCliArgs(argv); + expect(args['tls']).equals(true); + expect(args['port']).equals('1234'); }); - context('when providing no DB address', function () { - context('when providing a host', function () { - const argv = [...baseArgv, '--host', 'example.com']; + it(`replaces --sslPEMKeyFile with --tlsCertificateKeyFile`, function () { + const argv = [...baseArgv, `--sslPEMKeyFile`, `test`]; - it('sets the host value in the object', function () { - expect(parseCliArgs(argv).host).to.equal('example.com'); - }); - }); - - context('when providing a port', function () { - const argv = [...baseArgv, '--port', '20000']; - - it('sets the port value in the object', function () { - expect(parseCliArgs(argv).port).to.equal('20000'); - }); - }); + const args = parseMongoshCliArgs(argv); + expect(args).to.not.have.property('sslPEMKeyFile'); + expect(args['tlsCertificateKeyFile']).to.equal('test'); }); }); - - context('when providing a deprecated argument', function () { - for (const { deprecated, replacement, value } of [ - { deprecated: 'ssl', replacement: 'tls' }, - { - deprecated: 'sslAllowInvalidCertificates', - replacement: 'tlsAllowInvalidCertificates', - }, - { - deprecated: 'sslAllowInvalidCertificates', - replacement: 'tlsAllowInvalidCertificates', - }, - { - deprecated: 'sslAllowInvalidHostnames', - replacement: 'tlsAllowInvalidHostnames', - }, - // { deprecated: 'sslFIPSMode', replacement: 'tlsFIPSMode' }, <<-- FIPS is currently not supported right now - { - deprecated: 'sslPEMKeyFile', - replacement: 'tlsCertificateKeyFile', - value: 'pemKeyFile', - }, - { - deprecated: 'sslPEMKeyPassword', - replacement: 'tlsCertificateKeyFilePassword', - value: 'pemKeyPass', - }, - { deprecated: 'sslCAFile', replacement: 'tlsCAFile', value: 'caFile' }, - // { deprecated: 'sslCertificateSelector', replacement: 'tlsCertificateSelector', value: 'certSelector' }, <<-- Certificate selector not supported right now - { - deprecated: 'sslCRLFile', - replacement: 'tlsCRLFile', - value: 'crlFile', - }, - { - deprecated: 'sslDisabledProtocols', - replacement: 'tlsDisabledProtocols', - value: 'disabledProtos', - }, - ] as const) { - it(`replaces --${deprecated} with --${replacement}`, function () { - const argv = [...baseArgv, `--${deprecated}`]; - if (value) { - argv.push(value); - } - - const args = parseCliArgs(argv); - expect(args).to.not.have.property(deprecated); - expect(args[replacement]).to.equal(value ?? true); - }); - } - }); }); }); diff --git a/packages/cli-repl/src/arg-parser.ts b/packages/cli-repl/src/arg-parser.ts index 932fae1d46..4ff9762a4a 100644 --- a/packages/cli-repl/src/arg-parser.ts +++ b/packages/cli-repl/src/arg-parser.ts @@ -1,7 +1,8 @@ -import { CommonErrors, MongoshUnimplementedError } from '@mongosh/errors'; import i18n from '@mongosh/i18n'; -import type { CliOptions } from '@mongosh/arg-parser'; -import parser from 'yargs-parser'; +import { + parseCliArgs, + UnknownCliArgumentError, +} from '@mongosh/arg-parser/arg-parser'; import { colorizeForStderr as clr } from './clr'; import { USAGE } from './constants'; @@ -10,236 +11,21 @@ import { USAGE } from './constants'; */ const UNKNOWN = 'cli-repl.arg-parser.unknown-option'; -/** - * The yargs-parser options configuration. - */ -const OPTIONS = { - string: [ - 'apiVersion', - 'authenticationDatabase', - 'authenticationMechanism', - 'awsAccessKeyId', - 'awsIamSessionToken', - 'awsSecretAccessKey', - 'awsSessionToken', - 'awsIamSessionToken', - 'browser', - 'csfleLibraryPath', - 'cryptSharedLibPath', - 'db', - 'gssapiHostName', - 'gssapiServiceName', - 'sspiHostnameCanonicalization', - 'sspiRealmOverride', - 'jsContext', - 'host', - 'keyVaultNamespace', - 'kmsURL', - 'locale', - 'oidcFlows', - 'oidcRedirectUri', - 'password', - 'port', - 'sslPEMKeyFile', - 'sslPEMKeyPassword', - 'sslCAFile', - 'sslCertificateSelector', - 'sslCRLFile', - 'sslDisabledProtocols', - 'tlsCAFile', - 'tlsCertificateKeyFile', - 'tlsCertificateKeyFilePassword', - 'tlsCertificateSelector', - 'tlsCRLFile', - 'tlsDisabledProtocols', - 'username', - ], - boolean: [ - 'apiDeprecationErrors', - 'apiStrict', - 'buildInfo', - 'exposeAsyncRewriter', - 'help', - 'ipv6', - 'nodb', - 'norc', - 'oidcTrustedEndpoint', - 'oidcIdTokenAsAccessToken', - 'oidcNoNonce', - 'perfTests', - 'quiet', - 'retryWrites', - 'shell', - 'smokeTests', - 'skipStartupWarnings', - 'ssl', - 'sslAllowInvalidCertificates', - 'sslAllowInvalidHostnames', - 'sslFIPSMode', - 'tls', - 'tlsAllowInvalidCertificates', - 'tlsAllowInvalidHostnames', - 'tlsFIPSMode', - 'tlsUseSystemCA', - 'verbose', - 'version', - ], - array: ['eval', 'file'], - alias: { - h: 'help', - p: 'password', - u: 'username', - f: 'file', - 'build-info': 'buildInfo', - json: 'json', // List explicitly here since it can be a boolean or a string - browser: 'browser', // ditto - oidcDumpTokens: 'oidcDumpTokens', // ditto - oidcRedirectUrl: 'oidcRedirectUri', // I'd get this wrong about 50% of the time - oidcIDTokenAsAccessToken: 'oidcIdTokenAsAccessToken', // ditto - }, - configuration: { - 'camel-case-expansion': false, - 'unknown-options-as-args': true, - 'parse-positional-numbers': false, - 'parse-numbers': false, - 'greedy-arrays': false, - 'short-option-groups': false, - }, -}; - -/** - * Maps deprecated arguments to their new counterparts. - */ -const DEPRECATED_ARGS_WITH_REPLACEMENT: Record = { - ssl: 'tls', - sslAllowInvalidCertificates: 'tlsAllowInvalidCertificates', - sslAllowInvalidHostnames: 'tlsAllowInvalidHostnames', - sslFIPSMode: 'tlsFIPSMode', - sslPEMKeyFile: 'tlsCertificateKeyFile', - sslPEMKeyPassword: 'tlsCertificateKeyFilePassword', - sslCAFile: 'tlsCAFile', - sslCertificateSelector: 'tlsCertificateSelector', - sslCRLFile: 'tlsCRLFile', - sslDisabledProtocols: 'tlsDisabledProtocols', -}; - -/** - * If an unsupported argument is given an error will be thrown. - */ -const UNSUPPORTED_ARGS: Readonly = ['sslFIPSMode', 'gssapiHostName']; - -/** - * Determine the locale of the shell. - * - * @param {string[]} args - The arguments. - * - * @returns {string} The locale. - */ -export function getLocale(args: string[], env: any): string { - const localeIndex = args.indexOf('--locale'); - if (localeIndex > -1) { - return args[localeIndex + 1]; - } - const lang = env.LANG || env.LANGUAGE || env.LC_ALL || env.LC_MESSAGES; - return lang ? lang.split('.')[0] : lang; -} - -function isConnectionSpecifier(arg?: string): boolean { - return ( - typeof arg === 'string' && - (arg.startsWith('mongodb://') || - arg.startsWith('mongodb+srv://') || - !(arg.endsWith('.js') || arg.endsWith('.mongodb'))) - ); -} - -/** - * Parses arguments into a JS object. - * - * @param args - The CLI arguments. - * - * @returns The arguments as cli options. - */ -export function parseCliArgs(args: string[]): CliOptions & { - smokeTests: boolean; - perfTests: boolean; - buildInfo: boolean; - _argParseWarnings: string[]; -} { - const programArgs = args.slice(2); - i18n.setLocale(getLocale(programArgs, process.env)); - - const parsed = parser(programArgs, OPTIONS) as unknown as CliOptions & { - smokeTests: boolean; - perfTests: boolean; - buildInfo: boolean; - _argParseWarnings: string[]; - _?: string[]; - file?: string[]; - }; - const positionalArguments = parsed._ ?? []; - for (const arg of positionalArguments) { - if (arg.startsWith('-')) { +export function parseMongoshCliArgs( + args: string[] +): ReturnType { + try { + return parseCliArgs(args); + } catch (error) { + if (error instanceof UnknownCliArgumentError) { throw new Error( - ` ${clr(i18n.__(UNKNOWN), 'mongosh:error')} ${clr(String(arg), 'bold')} + ` ${clr(i18n.__(UNKNOWN), 'mongosh:error')} ${clr( + String(error.argument), + 'bold' + )} ${USAGE}` ); } + throw error; } - - if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) { - parsed.connectionSpecifier = positionalArguments.shift(); - } - parsed.fileNames = [...(parsed.file ?? []), ...positionalArguments]; - - // All positional arguments are either in connectionSpecifier or fileNames, - // and should only be accessed that way now. - delete parsed._; - - parsed._argParseWarnings = verifyCliArguments(parsed); - - return parsed; -} - -export function verifyCliArguments(args: any /* CliOptions */): string[] { - for (const unsupported of UNSUPPORTED_ARGS) { - if (unsupported in args) { - throw new MongoshUnimplementedError( - `Argument --${unsupported} is not supported in mongosh`, - CommonErrors.InvalidArgument - ); - } - } - - if (![undefined, true, false, 'relaxed', 'canonical'].includes(args.json)) { - throw new MongoshUnimplementedError( - '--json can only have the values relaxed or canonical', - CommonErrors.InvalidArgument - ); - } - - if ( - ![undefined, true, false, 'redacted', 'include-secrets'].includes( - args.oidcDumpTokens - ) - ) { - throw new MongoshUnimplementedError( - '--oidcDumpTokens can only have the values redacted or include-secrets', - CommonErrors.InvalidArgument - ); - } - - const messages = []; - for (const deprecated in DEPRECATED_ARGS_WITH_REPLACEMENT) { - if (deprecated in args) { - const replacement = DEPRECATED_ARGS_WITH_REPLACEMENT[deprecated]; - messages.push( - `WARNING: argument --${deprecated} is deprecated and will be removed. Use --${replacement} instead.` - ); - - args[replacement] = args[deprecated]; - delete args[deprecated]; - } - } - return messages; } diff --git a/packages/cli-repl/src/run.ts b/packages/cli-repl/src/run.ts index 7e18e526bf..612bf42ee1 100644 --- a/packages/cli-repl/src/run.ts +++ b/packages/cli-repl/src/run.ts @@ -15,7 +15,7 @@ enableFipsIfRequested(); import { markTime } from './startup-timing'; import { CliRepl } from './cli-repl'; -import { parseCliArgs } from './arg-parser'; +import { parseMongoshCliArgs } from './arg-parser'; import { runSmokeTests } from './smoke-tests'; import { USAGE } from './constants'; import { baseBuildInfo, buildInfo } from './build-info'; @@ -85,7 +85,7 @@ async function main() { try { (net as any)?.setDefaultAutoSelectFamily?.(true); - const options = parseCliArgs(process.argv); + const options = parseMongoshCliArgs(process.argv); for (const warning of options._argParseWarnings) { console.warn(warning); } diff --git a/packages/snippet-manager/package.json b/packages/snippet-manager/package.json index 4107966917..9db7e8faaa 100644 --- a/packages/snippet-manager/package.json +++ b/packages/snippet-manager/package.json @@ -42,8 +42,8 @@ "bson": "^6.10.4", "cross-spawn": "^7.0.5", "escape-string-regexp": "^4.0.0", - "zod": "^3.24.1", - "tar": "^6.1.15" + "tar": "^6.1.15", + "zod": "^3.25.76" }, "devDependencies": { "@mongodb-js/eslint-config-mongosh": "^1.0.0",