Skip to content

Commit 002ff53

Browse files
authored
feat: add global shell settings API MONGOSH-635 (#780)
1 parent c8cee80 commit 002ff53

24 files changed

+344
-88
lines changed

packages/autocomplete/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ async function completer(params: AutocompleteParameters, line: string): Promise<
5757
const AGG_CURSOR_COMPLETIONS = shellSignatures.AggregationCursor.attributes as TypeSignatureAttributes;
5858
const COLL_CURSOR_COMPLETIONS = shellSignatures.Cursor.attributes as TypeSignatureAttributes;
5959
const RS_COMPLETIONS = shellSignatures.ReplicaSet.attributes as TypeSignatureAttributes;
60+
const CONFIG_COMPLETIONS = shellSignatures.ShellConfig.attributes as TypeSignatureAttributes;
6061
const SHARD_COMPLETE = shellSignatures.Shard.attributes as TypeSignatureAttributes;
6162

6263
// keep initial line param intact to always return in return statement
@@ -132,6 +133,10 @@ async function completer(params: AutocompleteParameters, line: string): Promise<
132133
const hits = filterShellAPI(
133134
params, RS_COMPLETIONS, elToComplete, splitLine);
134135
return [hits.length ? hits : [], line];
136+
} else if (firstLineEl.match(/\bconfig\b/) && splitLine.length === 2) {
137+
const hits = filterShellAPI(
138+
params, CONFIG_COMPLETIONS, elToComplete, splitLine);
139+
return [hits.length ? hits : [], line];
135140
}
136141

137142
return [[], line];

packages/browser-runtime-electron/src/electron-runtime.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ describe('Electron runtime', function() {
2424
evaluationListener = sinon.createStubInstance(class FakeListener {});
2525
evaluationListener.onPrint = sinon.stub();
2626
electronRuntime = new ElectronRuntime(serviceProvider, messageBus);
27-
electronRuntime.setEvaluationListener(evaluationListener);
27+
electronRuntime.setEvaluationListener(evaluationListener as any);
2828
});
2929

3030
it('can evaluate simple js', async() => {

packages/cli-repl/src/cli-repl.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ describe('CliRepl', () => {
155155
expect.fail('expected error');
156156
});
157157

158+
it('returns the list of available config options when asked to', () => {
159+
expect(cliRepl.listConfigOptions()).to.deep.equal([
160+
'batchSize', 'enableTelemetry', 'inspectDepth', 'historyLength'
161+
]);
162+
});
163+
158164
context('loading JS files from disk', () => {
159165
it('allows loading a file from the disk', async() => {
160166
const filenameA = path.resolve(__dirname, '..', 'test', 'fixtures', 'load', 'a.js');

packages/cli-repl/src/cli-repl.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { CliReplErrors } from './error-codes';
1515
import { MongocryptdManager } from './mongocryptd-manager';
1616
import MongoshNodeRepl, { MongoshNodeReplOptions } from './mongosh-repl';
1717
import setupLoggerAndTelemetry from './setup-logger-and-telemetry';
18-
import { MongoshBus, UserConfig } from '@mongosh/types';
18+
import { MongoshBus, CliUserConfig } from '@mongosh/types';
1919
import { once } from 'events';
2020
import { createWriteStream, promises as fs } from 'fs';
2121
import path from 'path';
@@ -51,8 +51,8 @@ class CliRepl {
5151
cliOptions: CliOptions;
5252
mongocryptdManager: MongocryptdManager;
5353
shellHomeDirectory: ShellHomeDirectory;
54-
configDirectory: ConfigManager<UserConfig>;
55-
config: UserConfig = new UserConfig();
54+
configDirectory: ConfigManager<CliUserConfig>;
55+
config: CliUserConfig = new CliUserConfig();
5656
input: Readable;
5757
output: Writable;
5858
logId: string;
@@ -75,13 +75,13 @@ class CliRepl {
7575
this.onExit = options.onExit;
7676

7777
this.shellHomeDirectory = new ShellHomeDirectory(options.shellHomePaths);
78-
this.configDirectory = new ConfigManager<UserConfig>(
78+
this.configDirectory = new ConfigManager<CliUserConfig>(
7979
this.shellHomeDirectory)
8080
.on('error', (err: Error) =>
8181
this.bus.emit('mongosh:error', err))
82-
.on('new-config', (config: UserConfig) =>
82+
.on('new-config', (config: CliUserConfig) =>
8383
this.bus.emit('mongosh:new-user', config.userId, config.enableTelemetry))
84-
.on('update-config', (config: UserConfig) =>
84+
.on('update-config', (config: CliUserConfig) =>
8585
this.bus.emit('mongosh:update-user', config.userId, config.enableTelemetry));
8686

8787
this.mongocryptdManager = new MongocryptdManager(
@@ -146,11 +146,8 @@ class CliRepl {
146146
return this.analytics;
147147
});
148148

149-
this.config = {
150-
userId: new bson.ObjectId().toString(),
151-
enableTelemetry: true,
152-
disableGreetingMessage: false
153-
};
149+
this.config.userId = new bson.ObjectId().toString();
150+
this.config.enableTelemetry = true;
154151
try {
155152
this.config = await this.configDirectory.generateOrReadConfig(this.config);
156153
} catch (err) {
@@ -312,11 +309,11 @@ class CliRepl {
312309
return this.shellHomeDirectory.roamingPath('mongosh_repl_history');
313310
}
314311

315-
async getConfig<K extends keyof UserConfig>(key: K): Promise<UserConfig[K]> {
312+
async getConfig<K extends keyof CliUserConfig>(key: K): Promise<CliUserConfig[K]> {
316313
return this.config[key];
317314
}
318315

319-
async setConfig<K extends keyof UserConfig>(key: K, value: UserConfig[K]): Promise<void> {
316+
async setConfig<K extends keyof CliUserConfig>(key: K, value: CliUserConfig[K]): Promise<'success'> {
320317
this.config[key] = value;
321318
if (key === 'enableTelemetry') {
322319
this.bus.emit('mongosh:update-user', this.config.userId, this.config.enableTelemetry);
@@ -326,6 +323,12 @@ class CliRepl {
326323
} catch (err) {
327324
this.warnAboutInaccessibleFile(err, this.configDirectory.path());
328325
}
326+
return 'success';
327+
}
328+
329+
listConfigOptions(): string[] {
330+
const keys = Object.keys(this.config) as (keyof CliUserConfig)[];
331+
return keys.filter(key => key !== 'userId' && key !== 'disableGreetingMessage');
329332
}
330333

331334
async verifyNodeVersion(): Promise<void> {

packages/cli-repl/src/config-directory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export class ConfigManager<Config> extends EventEmitter {
7777
try {
7878
const config: Config = JSON.parse(await fd.readFile({ encoding: 'utf8' }));
7979
this.emit('update-config', config);
80-
return config;
80+
return { ...defaultConfig, ...config };
8181
} catch (err) {
8282
this.emit('error', err);
8383
return defaultConfig;

packages/cli-repl/src/format-output.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ type EvaluationResult = {
1616
type FormatOptions = {
1717
colors: boolean;
1818
depth?: number;
19+
maxArrayLength?: number;
20+
maxStringLength?: number;
1921
};
2022

2123
/**
@@ -34,11 +36,11 @@ export default function formatOutput(evaluationResult: EvaluationResult, options
3436
const { value, type } = evaluationResult;
3537

3638
if (type === 'Cursor' || type === 'AggregationCursor') {
37-
return formatCursor(value, options);
39+
return formatCursor(value, { ...options, maxArrayLength: Infinity });
3840
}
3941

4042
if (type === 'CursorIterationResult') {
41-
return formatCursorIterationResult(value, options);
43+
return formatCursorIterationResult(value, { ...options, maxArrayLength: Infinity });
4244
}
4345

4446
if (type === 'Help') {
@@ -96,7 +98,12 @@ Use db.getCollection('system.profile').find() to show raw profile entries.`, 'ye
9698
}
9799

98100
if (type === 'ExplainOutput' || type === 'ExplainableCursor') {
99-
return formatSimpleType(value, { ...options, depth: Infinity });
101+
return formatSimpleType(value, {
102+
...options,
103+
depth: Infinity,
104+
maxArrayLength: Infinity,
105+
maxStringLength: Infinity
106+
});
100107
}
101108

102109
return formatSimpleType(value, options);
@@ -165,12 +172,18 @@ export function formatError(error: Error, options: FormatOptions): string {
165172
return result;
166173
}
167174

175+
function removeUndefinedValues<T>(obj: T) {
176+
return Object.fromEntries(Object.entries(obj).filter(keyValue => keyValue[1] !== undefined));
177+
}
178+
168179
function inspect(output: any, options: FormatOptions): any {
169-
return util.inspect(output, {
180+
return util.inspect(output, removeUndefinedValues({
170181
showProxy: false,
171182
colors: options.colors ?? true,
172-
depth: options.depth ?? 6
173-
});
183+
depth: options.depth ?? 6,
184+
maxArrayLength: options.maxArrayLength,
185+
maxStringLength: options.maxStringLength
186+
}));
174187
}
175188

176189
function formatCursor(value: any, options: FormatOptions): any {

packages/cli-repl/src/mongosh-repl.spec.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('MongoshNodeRepl', () => {
4141
const cp = stubInterface<MongoshIOProvider>();
4242
cp.getHistoryFilePath.returns(path.join(tmpdir.path, 'history'));
4343
cp.getConfig.callsFake(async(key: string) => config[key]);
44-
cp.setConfig.callsFake(async(key: string, value: any) => { config[key] = value; });
44+
cp.setConfig.callsFake(async(key: string, value: any) => { config[key] = value; return 'success'; });
4545
cp.exit.callsFake(((code) => bus.emit('test-exit-event', code)) as any);
4646

4747
ioProvider = cp;
@@ -418,6 +418,41 @@ describe('MongoshNodeRepl', () => {
418418
});
419419
}
420420
});
421+
422+
context('with modified config values', () => {
423+
it('controls inspect depth', async() => {
424+
input.write('config.set("inspectDepth", 2)\n');
425+
await waitEval(bus);
426+
expect(output).to.include('Setting "inspectDepth" has been changed');
427+
428+
output = '';
429+
input.write('({a:{b:{c:{d:{e:{f:{g:{h:{}}}}}}}}})\n');
430+
await waitEval(bus);
431+
expect(stripAnsi(output).replace(/\s+/g, ' ')).to.include('{ a: { b: { c: [Object] } } }');
432+
433+
input.write('config.set("inspectDepth", 4)\n');
434+
await waitEval(bus);
435+
output = '';
436+
input.write('({a:{b:{c:{d:{e:{f:{g:{h:{}}}}}}}}})\n');
437+
await waitEval(bus);
438+
expect(stripAnsi(output).replace(/\s+/g, ' ')).to.include('{ a: { b: { c: { d: { e: [Object] } } } } }');
439+
});
440+
441+
it('controls history length', async() => {
442+
input.write('config.set("historyLength", 2)\n');
443+
await waitEval(bus);
444+
445+
let i = 2;
446+
while (!output.includes('65536')) {
447+
input.write(`${i} + ${i}\n`);
448+
await waitEval(bus);
449+
i *= 2;
450+
}
451+
452+
const { history } = mongoshRepl.runtimeState().repl as any;
453+
expect(history).to.have.lengthOf(2);
454+
});
455+
});
421456
});
422457

423458
context('with fake TTY', () => {

packages/cli-repl/src/mongosh-repl.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import completer from '@mongosh/autocomplete';
22
import { MongoshCommandFailed, MongoshInternalError, MongoshWarning } from '@mongosh/errors';
33
import { changeHistory } from '@mongosh/history';
4-
import i18n from '@mongosh/i18n';
54
import type { ServiceProvider, AutoEncryptionOptions } from '@mongosh/service-provider-core';
65
import { EvaluationListener, ShellCliOptions, ShellInternalState, OnLoadResult } from '@mongosh/shell-api';
76
import { ShellEvaluator, ShellResult } from '@mongosh/shell-evaluator';
8-
import type { MongoshBus, UserConfig } from '@mongosh/types';
7+
import type { MongoshBus, CliUserConfig, ConfigProvider } from '@mongosh/types';
98
import askpassword from 'askpassword';
109
import { Console } from 'console';
1110
import { once } from 'events';
@@ -25,10 +24,8 @@ export type MongoshCliOptions = ShellCliOptions & {
2524
quiet?: boolean;
2625
};
2726

28-
export type MongoshIOProvider = {
27+
export type MongoshIOProvider = ConfigProvider<CliUserConfig> & {
2928
getHistoryFilePath(): string;
30-
getConfig<K extends keyof UserConfig>(key: K): Promise<UserConfig[K]>;
31-
setConfig<K extends keyof UserConfig>(key: K, value: UserConfig[K]): Promise<void>;
3229
exit(code: number): Promise<never>;
3330
readFileUTF8(filename: string): Promise<{ contents: string, absolutePath: string }>;
3431
startMongocryptd(): Promise<AutoEncryptionOptions['extraOptions']>;
@@ -72,6 +69,7 @@ class MongoshNodeRepl implements EvaluationListener {
7269
ioProvider: MongoshIOProvider;
7370
onClearCommand?: EvaluationListener['onClearCommand'];
7471
insideAutoComplete: boolean;
72+
inspectDepth = 0;
7573

7674
constructor(options: MongoshNodeReplOptions) {
7775
this.input = options.input;
@@ -95,6 +93,8 @@ class MongoshNodeRepl implements EvaluationListener {
9593
await this.greet(mongodVersion);
9694
await this.printStartupLog(internalState);
9795

96+
this.inspectDepth = await this.getConfig('inspectDepth');
97+
9898
const repl = asyncRepl.start({
9999
start: prettyRepl.start,
100100
input: this.lineByLineInput as unknown as Readable,
@@ -104,7 +104,7 @@ class MongoshNodeRepl implements EvaluationListener {
104104
breakEvalOnSigint: true,
105105
preview: false,
106106
asyncEval: this.eval.bind(this),
107-
historySize: 1000, // Same as the old shell.
107+
historySize: await this.getConfig('historyLength'),
108108
wrapCallbackError:
109109
(err: Error) => Object.assign(new MongoshInternalError(err.message), { stack: err.stack }),
110110
...this.nodeReplOptions
@@ -262,9 +262,9 @@ class MongoshNodeRepl implements EvaluationListener {
262262
text += `Using MongoDB: ${mongodVersion}\n`;
263263
text += `${this.clr('Using Mongosh Beta', ['bold', 'yellow'])}: ${version}\n`;
264264
text += `${MONGOSH_WIKI}\n`;
265-
if (!await this.ioProvider.getConfig('disableGreetingMessage')) {
265+
if (!await this.getConfig('disableGreetingMessage')) {
266266
text += `${TELEMETRY_GREETING_MESSAGE}\n`;
267-
await this.ioProvider.setConfig('disableGreetingMessage', true);
267+
await this.setConfig('disableGreetingMessage', true);
268268
}
269269
this.output.write(text);
270270
}
@@ -381,16 +381,6 @@ class MongoshNodeRepl implements EvaluationListener {
381381
return this.formatOutput({ type: result.type, value: result.printable });
382382
}
383383

384-
async toggleTelemetry(enabled: boolean): Promise<string> {
385-
await this.ioProvider.setConfig('enableTelemetry', enabled);
386-
387-
if (enabled) {
388-
return i18n.__('cli-repl.cli-repl.enabledTelemetry');
389-
}
390-
391-
return i18n.__('cli-repl.cli-repl.disabledTelemetry');
392-
}
393-
394384
onPrint(values: ShellResult[]): void {
395385
const joined = values.map((value) => this.writer(value)).join(' ');
396386
this.output.write(joined + '\n');
@@ -419,11 +409,12 @@ class MongoshNodeRepl implements EvaluationListener {
419409
return clr(text, style, this.getFormatOptions());
420410
}
421411

422-
getFormatOptions(): { colors: boolean } {
412+
getFormatOptions(): { colors: boolean, depth: number } {
423413
const output = this.output as WriteStream;
424414
return {
425415
colors: this._runtimeState?.repl?.useColors ??
426-
(output.isTTY && output.getColorDepth() > 1)
416+
(output.isTTY && output.getColorDepth() > 1),
417+
depth: this.inspectDepth
427418
};
428419
}
429420

@@ -451,6 +442,24 @@ class MongoshNodeRepl implements EvaluationListener {
451442
return this.ioProvider.exit(0);
452443
}
453444

445+
async getConfig<K extends keyof CliUserConfig>(key: K): Promise<CliUserConfig[K]> {
446+
return this.ioProvider.getConfig(key);
447+
}
448+
449+
async setConfig<K extends keyof CliUserConfig>(key: K, value: CliUserConfig[K]): Promise<'success' | 'ignored'> {
450+
if (key === 'historyLength' && this._runtimeState) {
451+
(this.runtimeState().repl as any).historySize = value;
452+
}
453+
if (key === 'inspectDepth') {
454+
this.inspectDepth = +value;
455+
}
456+
return this.ioProvider.setConfig(key, value);
457+
}
458+
459+
listConfigOptions(): Promise<string[]> | string[] {
460+
return this.ioProvider.listConfigOptions();
461+
}
462+
454463
async startMongocryptd(): Promise<AutoEncryptionOptions['extraOptions']> {
455464
return this.ioProvider.startMongocryptd();
456465
}

packages/i18n/src/locales/en_US.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,19 @@ const translations: Catalog = {
150150
}
151151
},
152152
},
153+
ShellConfig: {
154+
help: {
155+
description: 'Shell configuration methods',
156+
attributes: {
157+
get: {
158+
description: 'Get a configuration value with config.get(key)'
159+
},
160+
set: {
161+
description: 'Change a configuration value with config.set(key, value)'
162+
}
163+
}
164+
},
165+
},
153166
AggregationCursor: {
154167
help: {
155168
description: 'Aggregation Class',

packages/node-runtime-worker-thread/src/child-process-evaluation-listener.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ export class ChildProcessEvaluationListener {
1717
onPrint(values) {
1818
return workerRuntime.evaluationListener?.onPrint?.(values);
1919
},
20-
toggleTelemetry(enabled) {
21-
return workerRuntime.evaluationListener?.toggleTelemetry?.(enabled);
20+
setConfig(key, value) {
21+
return workerRuntime.evaluationListener?.setConfig?.(key, value) ?? Promise.resolve('ignored');
22+
},
23+
getConfig(key) {
24+
return workerRuntime.evaluationListener?.getConfig?.(key) as any;
25+
},
26+
listConfigOptions() {
27+
return workerRuntime.evaluationListener?.listConfigOptions?.() as any;
2228
},
2329
onClearCommand() {
2430
return workerRuntime.evaluationListener?.onClearCommand?.();

0 commit comments

Comments
 (0)