Skip to content

Commit 7905894

Browse files
committed
chore: add arg-parser and put the config under test
This commit is biggish because it adds a big chunk of tests that weren't there before. While it would be arguably that testing every flag might be too much, because I'm changing how arguments are parsed, I want to have this as a safeline.
1 parent 42837a4 commit 7905894

File tree

4 files changed

+633
-14
lines changed

4 files changed

+633
-14
lines changed

package-lock.json

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"@mongodb-js/device-id": "^0.3.1",
7676
"@mongodb-js/devtools-connect": "^3.9.2",
7777
"@mongodb-js/devtools-proxy-support": "^0.5.1",
78+
"@mongosh/arg-parser": "^3.14.0",
7879
"@mongosh/service-provider-node-driver": "^3.10.2",
7980
"@vitest/eslint-plugin": "^1.3.4",
8081
"bson": "^6.10.4",

src/common/config.ts

Lines changed: 172 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,104 @@
11
import path from "path";
22
import os from "os";
33
import argv from "yargs-parser";
4-
4+
import type { CliOptions } from "@mongosh/arg-parser";
55
import { ReadConcernLevel, ReadPreferenceMode, W } from "mongodb";
66

7+
// From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts
8+
const OPTIONS = {
9+
string: [
10+
"apiBaseUrl",
11+
"apiClientId",
12+
"apiClientSecret",
13+
"connectionString",
14+
"httpHost",
15+
"httpPort",
16+
"idleTimeoutMs",
17+
"logPath",
18+
"notificationTimeoutMs",
19+
"telemetry",
20+
"transport",
21+
"apiVersion",
22+
"authenticationDatabase",
23+
"authenticationMechanism",
24+
"browser",
25+
"db",
26+
"gssapiHostName",
27+
"gssapiServiceName",
28+
"host",
29+
"oidcFlows",
30+
"oidcRedirectUri",
31+
"password",
32+
"port",
33+
"sslCAFile",
34+
"sslCRLFile",
35+
"sslCertificateSelector",
36+
"sslDisabledProtocols",
37+
"sslPEMKeyFile",
38+
"sslPEMKeyPassword",
39+
"sspiHostnameCanonicalization",
40+
"sspiRealmOverride",
41+
"tlsCAFile",
42+
"tlsCRLFile",
43+
"tlsCertificateKeyFile",
44+
"tlsCertificateKeyFilePassword",
45+
"tlsCertificateSelector",
46+
"tlsDisabledProtocols",
47+
"username",
48+
],
49+
boolean: [
50+
"apiDeprecationErrors",
51+
"apiStrict",
52+
"help",
53+
"indexCheck",
54+
"ipv6",
55+
"nodb",
56+
"oidcIdTokenAsAccessToken",
57+
"oidcNoNonce",
58+
"oidcTrustedEndpoint",
59+
"readOnly",
60+
"retryWrites",
61+
"ssl",
62+
"sslAllowInvalidCertificates",
63+
"sslAllowInvalidHostnames",
64+
"sslFIPSMode",
65+
"tls",
66+
"tlsAllowInvalidCertificates",
67+
"tlsAllowInvalidHostnames",
68+
"tlsFIPSMode",
69+
"tlsUseSystemCA",
70+
"version",
71+
],
72+
array: ["disabledTools", "loggers"],
73+
alias: {
74+
h: "help",
75+
p: "password",
76+
u: "username",
77+
"build-info": "buildInfo",
78+
browser: "browser",
79+
oidcDumpTokens: "oidcDumpTokens",
80+
oidcRedirectUrl: "oidcRedirectUri",
81+
oidcIDTokenAsAccessToken: "oidcIdTokenAsAccessToken",
82+
},
83+
configuration: {
84+
"camel-case-expansion": false,
85+
"unknown-options-as-args": true,
86+
"parse-positional-numbers": false,
87+
"parse-numbers": false,
88+
"greedy-arrays": true,
89+
"short-option-groups": false,
90+
},
91+
};
92+
93+
function isConnectionSpecifier(arg: string | undefined): boolean {
94+
return (
95+
arg !== undefined &&
96+
(arg.startsWith("mongodb://") ||
97+
arg.startsWith("mongodb+srv://") ||
98+
!(arg.endsWith(".js") || arg.endsWith(".mongodb")))
99+
);
100+
}
101+
7102
export interface ConnectOptions {
8103
readConcern: ReadConcernLevel;
9104
readPreference: ReadPreferenceMode;
@@ -13,7 +108,7 @@ export interface ConnectOptions {
13108

14109
// If we decide to support non-string config options, we'll need to extend the mechanism for parsing
15110
// env variables.
16-
export interface UserConfig {
111+
export interface UserConfig extends CliOptions {
17112
apiBaseUrl: string;
18113
apiClientId?: string;
19114
apiClientSecret?: string;
@@ -53,11 +148,11 @@ const defaults: UserConfig = {
53148
notificationTimeoutMs: 540000, // 9 minutes
54149
};
55150

56-
export const config = {
57-
...defaults,
58-
...getEnvConfig(),
59-
...getCliConfig(),
60-
};
151+
export const config = setupUserConfig({
152+
defaults,
153+
cli: process.argv,
154+
env: process.env,
155+
});
61156

62157
function getLogPath(): string {
63158
const localDataPath =
@@ -73,7 +168,7 @@ function getLogPath(): string {
73168
// Gets the config supplied by the user as environment variables. The variable names
74169
// are prefixed with `MDB_MCP_` and the keys match the UserConfig keys, but are converted
75170
// to SNAKE_UPPER_CASE.
76-
function getEnvConfig(): Partial<UserConfig> {
171+
function parseEnvConfig(env: Record<string, unknown>): Partial<UserConfig> {
77172
function setValue(obj: Record<string, unknown>, path: string[], value: string): void {
78173
const currentField = path.shift();
79174
if (!currentField) {
@@ -110,7 +205,7 @@ function getEnvConfig(): Partial<UserConfig> {
110205
}
111206

112207
const result: Record<string, unknown> = {};
113-
const mcpVariables = Object.entries(process.env).filter(
208+
const mcpVariables = Object.entries(env).filter(
114209
([key, value]) => value !== undefined && key.startsWith("MDB_MCP_")
115210
) as [string, string][];
116211
for (const [key, value] of mcpVariables) {
@@ -129,9 +224,72 @@ function SNAKE_CASE_toCamelCase(str: string): string {
129224
return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", ""));
130225
}
131226

132-
// Reads the cli args and parses them into a UserConfig object.
133-
function getCliConfig() {
134-
return argv(process.argv.slice(2), {
135-
array: ["disabledTools", "loggers"],
136-
}) as unknown as Partial<UserConfig>;
227+
// Right now we have arguments that are not compatible with the format used in mongosh.
228+
// An example is using --connectionString and positional arguments.
229+
// We will consolidate them in a way where the mongosh format takes precedence.
230+
// We will warn users that previous configuration is deprecated in favour of
231+
// whatever is in mongosh.
232+
function parseCliConfig(args: string[]): CliOptions {
233+
const programArgs = args.slice(2);
234+
const parsed = argv(programArgs, OPTIONS) as unknown as CliOptions &
235+
UserConfig & {
236+
_?: string[];
237+
};
238+
239+
const positionalArguments = parsed._ ?? [];
240+
if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) {
241+
parsed.connectionSpecifier = positionalArguments.shift();
242+
}
243+
244+
delete parsed._;
245+
return parsed;
246+
}
247+
248+
function commaSeparatedToArray<T extends string[]>(str: string | string[] | undefined): T {
249+
if (str === undefined) {
250+
return [] as unknown as T;
251+
}
252+
253+
if (!Array.isArray(str)) {
254+
return [str] as T;
255+
}
256+
257+
if (str.length === 0) {
258+
return str as T;
259+
}
260+
261+
if (str.length === 1) {
262+
if (str[0]?.indexOf(",") === -1) {
263+
return str as T;
264+
} else {
265+
return str[0]?.split(",").map((e) => e.trim()) as T;
266+
}
267+
}
268+
269+
return str as T;
270+
}
271+
272+
export function setupUserConfig({
273+
cli,
274+
env,
275+
defaults,
276+
}: {
277+
cli: string[];
278+
env: Record<string, unknown>;
279+
defaults: Partial<UserConfig>;
280+
}): UserConfig {
281+
const userConfig: UserConfig = {
282+
...defaults,
283+
...parseEnvConfig(env),
284+
...parseCliConfig(cli),
285+
} as UserConfig;
286+
287+
userConfig.disabledTools = commaSeparatedToArray(userConfig.disabledTools);
288+
userConfig.loggers = commaSeparatedToArray(userConfig.loggers);
289+
290+
if (userConfig.connectionString && userConfig.connectionSpecifier) {
291+
delete userConfig.connectionString;
292+
}
293+
294+
return userConfig;
137295
}

0 commit comments

Comments
 (0)