Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/autocomplete/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ async function completer(params: AutocompleteParameters, line: string): Promise<
const AGG_CURSOR_COMPLETIONS = shellSignatures.AggregationCursor.attributes as TypeSignatureAttributes;
const COLL_CURSOR_COMPLETIONS = shellSignatures.Cursor.attributes as TypeSignatureAttributes;
const RS_COMPLETIONS = shellSignatures.ReplicaSet.attributes as TypeSignatureAttributes;
const CONFIG_COMPLETIONS = shellSignatures.ShellConfig.attributes as TypeSignatureAttributes;
const SHARD_COMPLETE = shellSignatures.Shard.attributes as TypeSignatureAttributes;

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

return [[], line];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('Electron runtime', function() {
evaluationListener = sinon.createStubInstance(class FakeListener {});
evaluationListener.onPrint = sinon.stub();
electronRuntime = new ElectronRuntime(serviceProvider, messageBus);
electronRuntime.setEvaluationListener(evaluationListener);
electronRuntime.setEvaluationListener(evaluationListener as any);
});

it('can evaluate simple js', async() => {
Expand Down
6 changes: 6 additions & 0 deletions packages/cli-repl/src/cli-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ describe('CliRepl', () => {
expect.fail('expected error');
});

it('returns the list of available config options when asked to', () => {
expect(cliRepl.listConfigOptions()).to.deep.equal([
'batchSize', 'enableTelemetry', 'inspectDepth', 'historyLength'
]);
});

context('loading JS files from disk', () => {
it('allows loading a file from the disk', async() => {
const filenameA = path.resolve(__dirname, '..', 'test', 'fixtures', 'load', 'a.js');
Expand Down
29 changes: 16 additions & 13 deletions packages/cli-repl/src/cli-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { CliReplErrors } from './error-codes';
import { MongocryptdManager } from './mongocryptd-manager';
import MongoshNodeRepl, { MongoshNodeReplOptions } from './mongosh-repl';
import setupLoggerAndTelemetry from './setup-logger-and-telemetry';
import { MongoshBus, UserConfig } from '@mongosh/types';
import { MongoshBus, CliUserConfig } from '@mongosh/types';
import { once } from 'events';
import { createWriteStream, promises as fs } from 'fs';
import path from 'path';
Expand Down Expand Up @@ -51,8 +51,8 @@ class CliRepl {
cliOptions: CliOptions;
mongocryptdManager: MongocryptdManager;
shellHomeDirectory: ShellHomeDirectory;
configDirectory: ConfigManager<UserConfig>;
config: UserConfig = new UserConfig();
configDirectory: ConfigManager<CliUserConfig>;
config: CliUserConfig = new CliUserConfig();
input: Readable;
output: Writable;
logId: string;
Expand All @@ -75,13 +75,13 @@ class CliRepl {
this.onExit = options.onExit;

this.shellHomeDirectory = new ShellHomeDirectory(options.shellHomePaths);
this.configDirectory = new ConfigManager<UserConfig>(
this.configDirectory = new ConfigManager<CliUserConfig>(
this.shellHomeDirectory)
.on('error', (err: Error) =>
this.bus.emit('mongosh:error', err))
.on('new-config', (config: UserConfig) =>
.on('new-config', (config: CliUserConfig) =>
this.bus.emit('mongosh:new-user', config.userId, config.enableTelemetry))
.on('update-config', (config: UserConfig) =>
.on('update-config', (config: CliUserConfig) =>
this.bus.emit('mongosh:update-user', config.userId, config.enableTelemetry));

this.mongocryptdManager = new MongocryptdManager(
Expand Down Expand Up @@ -146,11 +146,8 @@ class CliRepl {
return this.analytics;
});

this.config = {
userId: new bson.ObjectId().toString(),
enableTelemetry: true,
disableGreetingMessage: false
};
this.config.userId = new bson.ObjectId().toString();
this.config.enableTelemetry = true;
try {
this.config = await this.configDirectory.generateOrReadConfig(this.config);
} catch (err) {
Expand Down Expand Up @@ -312,11 +309,11 @@ class CliRepl {
return this.shellHomeDirectory.roamingPath('mongosh_repl_history');
}

async getConfig<K extends keyof UserConfig>(key: K): Promise<UserConfig[K]> {
async getConfig<K extends keyof CliUserConfig>(key: K): Promise<CliUserConfig[K]> {
return this.config[key];
}

async setConfig<K extends keyof UserConfig>(key: K, value: UserConfig[K]): Promise<void> {
async setConfig<K extends keyof CliUserConfig>(key: K, value: CliUserConfig[K]): Promise<'success'> {
this.config[key] = value;
if (key === 'enableTelemetry') {
this.bus.emit('mongosh:update-user', this.config.userId, this.config.enableTelemetry);
Expand All @@ -326,6 +323,12 @@ class CliRepl {
} catch (err) {
this.warnAboutInaccessibleFile(err, this.configDirectory.path());
}
return 'success';
}

listConfigOptions(): string[] {
const keys = Object.keys(this.config) as (keyof CliUserConfig)[];
return keys.filter(key => key !== 'userId' && key !== 'disableGreetingMessage');
}

async verifyNodeVersion(): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-repl/src/config-directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class ConfigManager<Config> extends EventEmitter {
try {
const config: Config = JSON.parse(await fd.readFile({ encoding: 'utf8' }));
this.emit('update-config', config);
return config;
return { ...defaultConfig, ...config };
} catch (err) {
this.emit('error', err);
return defaultConfig;
Expand Down
25 changes: 19 additions & 6 deletions packages/cli-repl/src/format-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type EvaluationResult = {
type FormatOptions = {
colors: boolean;
depth?: number;
maxArrayLength?: number;
maxStringLength?: number;
};

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

if (type === 'Cursor' || type === 'AggregationCursor') {
return formatCursor(value, options);
return formatCursor(value, { ...options, maxArrayLength: Infinity });
}

if (type === 'CursorIterationResult') {
return formatCursorIterationResult(value, options);
return formatCursorIterationResult(value, { ...options, maxArrayLength: Infinity });
}

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

if (type === 'ExplainOutput' || type === 'ExplainableCursor') {
return formatSimpleType(value, { ...options, depth: Infinity });
return formatSimpleType(value, {
...options,
depth: Infinity,
maxArrayLength: Infinity,
maxStringLength: Infinity
});
}

return formatSimpleType(value, options);
Expand Down Expand Up @@ -165,12 +172,18 @@ export function formatError(error: Error, options: FormatOptions): string {
return result;
}

function removeUndefinedValues<T>(obj: T) {
return Object.fromEntries(Object.entries(obj).filter(keyValue => keyValue[1] !== undefined));
}

function inspect(output: any, options: FormatOptions): any {
return util.inspect(output, {
return util.inspect(output, removeUndefinedValues({
showProxy: false,
colors: options.colors ?? true,
depth: options.depth ?? 6
});
depth: options.depth ?? 6,
maxArrayLength: options.maxArrayLength,
maxStringLength: options.maxStringLength
}));
}

function formatCursor(value: any, options: FormatOptions): any {
Expand Down
37 changes: 36 additions & 1 deletion packages/cli-repl/src/mongosh-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('MongoshNodeRepl', () => {
const cp = stubInterface<MongoshIOProvider>();
cp.getHistoryFilePath.returns(path.join(tmpdir.path, 'history'));
cp.getConfig.callsFake(async(key: string) => config[key]);
cp.setConfig.callsFake(async(key: string, value: any) => { config[key] = value; });
cp.setConfig.callsFake(async(key: string, value: any) => { config[key] = value; return 'success'; });
cp.exit.callsFake(((code) => bus.emit('test-exit-event', code)) as any);

ioProvider = cp;
Expand Down Expand Up @@ -418,6 +418,41 @@ describe('MongoshNodeRepl', () => {
});
}
});

context('with modified config values', () => {
it('controls inspect depth', async() => {
input.write('config.set("inspectDepth", 2)\n');
await waitEval(bus);
expect(output).to.include('Setting "inspectDepth" has been changed');

output = '';
input.write('({a:{b:{c:{d:{e:{f:{g:{h:{}}}}}}}}})\n');
await waitEval(bus);
expect(stripAnsi(output).replace(/\s+/g, ' ')).to.include('{ a: { b: { c: [Object] } } }');

input.write('config.set("inspectDepth", 4)\n');
await waitEval(bus);
output = '';
input.write('({a:{b:{c:{d:{e:{f:{g:{h:{}}}}}}}}})\n');
await waitEval(bus);
expect(stripAnsi(output).replace(/\s+/g, ' ')).to.include('{ a: { b: { c: { d: { e: [Object] } } } } }');
});

it('controls history length', async() => {
input.write('config.set("historyLength", 2)\n');
await waitEval(bus);

let i = 2;
while (!output.includes('65536')) {
input.write(`${i} + ${i}\n`);
await waitEval(bus);
i *= 2;
}

const { history } = mongoshRepl.runtimeState().repl as any;
expect(history).to.have.lengthOf(2);
});
});
});

context('with fake TTY', () => {
Expand Down
49 changes: 29 additions & 20 deletions packages/cli-repl/src/mongosh-repl.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import completer from '@mongosh/autocomplete';
import { MongoshCommandFailed, MongoshInternalError, MongoshWarning } from '@mongosh/errors';
import { changeHistory } from '@mongosh/history';
import i18n from '@mongosh/i18n';
import type { ServiceProvider, AutoEncryptionOptions } from '@mongosh/service-provider-core';
import { EvaluationListener, ShellCliOptions, ShellInternalState, OnLoadResult } from '@mongosh/shell-api';
import { ShellEvaluator, ShellResult } from '@mongosh/shell-evaluator';
import type { MongoshBus, UserConfig } from '@mongosh/types';
import type { MongoshBus, CliUserConfig, ConfigProvider } from '@mongosh/types';
import askpassword from 'askpassword';
import { Console } from 'console';
import { once } from 'events';
Expand All @@ -25,10 +24,8 @@ export type MongoshCliOptions = ShellCliOptions & {
quiet?: boolean;
};

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

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

this.inspectDepth = await this.getConfig('inspectDepth');

const repl = asyncRepl.start({
start: prettyRepl.start,
input: this.lineByLineInput as unknown as Readable,
Expand All @@ -104,7 +104,7 @@ class MongoshNodeRepl implements EvaluationListener {
breakEvalOnSigint: true,
preview: false,
asyncEval: this.eval.bind(this),
historySize: 1000, // Same as the old shell.
historySize: await this.getConfig('historyLength'),
wrapCallbackError:
(err: Error) => Object.assign(new MongoshInternalError(err.message), { stack: err.stack }),
...this.nodeReplOptions
Expand Down Expand Up @@ -262,9 +262,9 @@ class MongoshNodeRepl implements EvaluationListener {
text += `Using MongoDB: ${mongodVersion}\n`;
text += `${this.clr('Using Mongosh Beta', ['bold', 'yellow'])}: ${version}\n`;
text += `${MONGOSH_WIKI}\n`;
if (!await this.ioProvider.getConfig('disableGreetingMessage')) {
if (!await this.getConfig('disableGreetingMessage')) {
text += `${TELEMETRY_GREETING_MESSAGE}\n`;
await this.ioProvider.setConfig('disableGreetingMessage', true);
await this.setConfig('disableGreetingMessage', true);
}
this.output.write(text);
}
Expand Down Expand Up @@ -381,16 +381,6 @@ class MongoshNodeRepl implements EvaluationListener {
return this.formatOutput({ type: result.type, value: result.printable });
}

async toggleTelemetry(enabled: boolean): Promise<string> {
await this.ioProvider.setConfig('enableTelemetry', enabled);

if (enabled) {
return i18n.__('cli-repl.cli-repl.enabledTelemetry');
}

return i18n.__('cli-repl.cli-repl.disabledTelemetry');
}

onPrint(values: ShellResult[]): void {
const joined = values.map((value) => this.writer(value)).join(' ');
this.output.write(joined + '\n');
Expand Down Expand Up @@ -419,11 +409,12 @@ class MongoshNodeRepl implements EvaluationListener {
return clr(text, style, this.getFormatOptions());
}

getFormatOptions(): { colors: boolean } {
getFormatOptions(): { colors: boolean, depth: number } {
const output = this.output as WriteStream;
return {
colors: this._runtimeState?.repl?.useColors ??
(output.isTTY && output.getColorDepth() > 1)
(output.isTTY && output.getColorDepth() > 1),
depth: this.inspectDepth
};
}

Expand Down Expand Up @@ -451,6 +442,24 @@ class MongoshNodeRepl implements EvaluationListener {
return this.ioProvider.exit(0);
}

async getConfig<K extends keyof CliUserConfig>(key: K): Promise<CliUserConfig[K]> {
return this.ioProvider.getConfig(key);
}

async setConfig<K extends keyof CliUserConfig>(key: K, value: CliUserConfig[K]): Promise<'success' | 'ignored'> {
if (key === 'historyLength' && this._runtimeState) {
(this.runtimeState().repl as any).historySize = value;
}
if (key === 'inspectDepth') {
this.inspectDepth = +value;
}
return this.ioProvider.setConfig(key, value);
}

listConfigOptions(): Promise<string[]> | string[] {
return this.ioProvider.listConfigOptions();
}

async startMongocryptd(): Promise<AutoEncryptionOptions['extraOptions']> {
return this.ioProvider.startMongocryptd();
}
Expand Down
13 changes: 13 additions & 0 deletions packages/i18n/src/locales/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,19 @@ const translations: Catalog = {
}
},
},
ShellConfig: {
help: {
description: 'Shell configuration methods',
attributes: {
get: {
description: 'Get a configuration value with config.get(key)'
},
set: {
description: 'Change a configuration value with config.set(key, value)'
}
}
},
},
AggregationCursor: {
help: {
description: 'Aggregation Class',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ export class ChildProcessEvaluationListener {
onPrint(values) {
return workerRuntime.evaluationListener?.onPrint?.(values);
},
toggleTelemetry(enabled) {
return workerRuntime.evaluationListener?.toggleTelemetry?.(enabled);
setConfig(key, value) {
return workerRuntime.evaluationListener?.setConfig?.(key, value) ?? Promise.resolve('ignored');
},
getConfig(key) {
return workerRuntime.evaluationListener?.getConfig?.(key) as any;
},
listConfigOptions() {
return workerRuntime.evaluationListener?.listConfigOptions?.() as any;
},
onClearCommand() {
return workerRuntime.evaluationListener?.onClearCommand?.();
Expand Down
Loading