Skip to content

Commit e65c0bd

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 974fa36 commit e65c0bd

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
@@ -94,6 +94,7 @@
9494
"@mongodb-js/device-id": "^0.3.1",
9595
"@mongodb-js/devtools-connect": "^3.9.2",
9696
"@mongodb-js/devtools-proxy-support": "^0.5.1",
97+
"@mongosh/arg-parser": "^3.14.0",
9798
"@mongosh/service-provider-node-driver": "^3.10.2",
9899
"@vitest/eslint-plugin": "^1.3.4",
99100
"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;
@@ -59,11 +154,11 @@ const defaults: UserConfig = {
59154
notificationTimeoutMs: 540000, // 9 minutes
60155
};
61156

62-
export const config = {
63-
...defaults,
64-
...getEnvConfig(),
65-
...getCliConfig(),
66-
};
157+
export const config = setupUserConfig({
158+
defaults,
159+
cli: process.argv,
160+
env: process.env,
161+
});
67162

68163
function getLocalDataPath(): string {
69164
return process.platform === "win32"
@@ -83,7 +178,7 @@ function getExportsPath(): string {
83178
// Gets the config supplied by the user as environment variables. The variable names
84179
// are prefixed with `MDB_MCP_` and the keys match the UserConfig keys, but are converted
85180
// to SNAKE_UPPER_CASE.
86-
function getEnvConfig(): Partial<UserConfig> {
181+
function parseEnvConfig(env: Record<string, unknown>): Partial<UserConfig> {
87182
function setValue(obj: Record<string, unknown>, path: string[], value: string): void {
88183
const currentField = path.shift();
89184
if (!currentField) {
@@ -120,7 +215,7 @@ function getEnvConfig(): Partial<UserConfig> {
120215
}
121216

122217
const result: Record<string, unknown> = {};
123-
const mcpVariables = Object.entries(process.env).filter(
218+
const mcpVariables = Object.entries(env).filter(
124219
([key, value]) => value !== undefined && key.startsWith("MDB_MCP_")
125220
) as [string, string][];
126221
for (const [key, value] of mcpVariables) {
@@ -139,9 +234,72 @@ function SNAKE_CASE_toCamelCase(str: string): string {
139234
return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", ""));
140235
}
141236

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

0 commit comments

Comments
 (0)