Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
40a8727
WIP
addaleax Jun 3, 2024
4b2991c
use DeepInspectServiceProviderWrapper, install our own inspect functi…
lerouxb Nov 20, 2025
f85dc9f
you cannot extend the return value of initializeBulkOp
lerouxb Nov 20, 2025
24ef043
apparently inspect passed as the final parameter isn't always a thing
lerouxb Nov 20, 2025
cda077c
better name
lerouxb Nov 24, 2025
7d008f7
inspect a shallow copy
lerouxb Nov 24, 2025
a27b349
fix comment
lerouxb Nov 24, 2025
8c523bb
leave bson values alone, don't accidentally override existing custom …
lerouxb Nov 24, 2025
aa2e830
don't remove things
lerouxb Nov 24, 2025
6307dd3
wrap every type of cursor
lerouxb Nov 24, 2025
70aa3ee
don't accidentally override our custom Date and RegExp inpect functions
lerouxb Nov 24, 2025
8624b68
don't depend on inspect
lerouxb Nov 24, 2025
cae4017
more indirection
lerouxb Nov 24, 2025
7d2d0a3
fill out more things on the stub
lerouxb Nov 24, 2025
d12548c
how did this work before?
lerouxb Nov 24, 2025
12fc59b
Merge branch 'main' into print-output-full
lerouxb Nov 25, 2025
22e67f8
add the custom inspect symbol as not-enumerable
lerouxb Nov 25, 2025
f4f64bc
pull bsonLibrary off _sp rather
lerouxb Nov 25, 2025
709de79
Merge branch 'main' into print-output-full
lerouxb Nov 26, 2025
77f64f2
don't wrap the service provider in java land
lerouxb Nov 27, 2025
72d8679
Update packages/shell-api/src/custom-inspect.ts
lerouxb Nov 27, 2025
a93b8fd
some unit tests
lerouxb Nov 27, 2025
0ce95c3
more unit tests
lerouxb Nov 27, 2025
69d3d13
adjust runtime indepdendence tests
lerouxb Nov 27, 2025
209c9f5
Update packages/shell-api/src/custom-inspect.ts
lerouxb Nov 27, 2025
c023d46
fixup: remove wrappable flag, skip async iter support for now, use fn…
addaleax Nov 27, 2025
e31ba35
fixup: merge cursor implementations, move to separate subdir
addaleax Nov 27, 2025
2bc07c3
fixup: fix runtime independence test again
addaleax Nov 27, 2025
370288f
fixup: add e2e tests
addaleax Nov 27, 2025
28dd205
fixup: oidc test
addaleax Nov 27, 2025
7202eb2
fixup: stub out more cli-repl test sp usage
addaleax Nov 28, 2025
1b72c5b
feat(cli-repl): add flag to control deep inspect behavior
addaleax Nov 28, 2025
f783da4
fixup: prettier doesnt run on subdirs?
addaleax Nov 28, 2025
0a9c1bf
fixup: add missing waitForPrompt call
addaleax Nov 28, 2025
caaa723
fixup: simplify using waitForCleanOutput
addaleax Nov 28, 2025
02e70c7
Merge remote-tracking branch 'origin/main' into print-output-full
addaleax Nov 28, 2025
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
1 change: 1 addition & 0 deletions packages/arg-parser/src/arg-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const OPTIONS = {
'apiDeprecationErrors',
'apiStrict',
'buildInfo',
'deepInspect',
'exposeAsyncRewriter',
'help',
'ipv6',
Expand Down
1 change: 1 addition & 0 deletions packages/arg-parser/src/cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface CliOptions {
csfleLibraryPath?: string;
cryptSharedLibPath?: string;
db?: string;
deepInspect?: boolean; // defaults to true
eval?: string[];
exposeAsyncRewriter?: boolean; // internal testing only
gssapiServiceName?: string;
Expand Down
11 changes: 9 additions & 2 deletions packages/cli-repl/src/cli-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import type {
DevtoolsProxyOptions,
} from '@mongodb-js/devtools-proxy-support';
import { useOrCreateAgent } from '@mongodb-js/devtools-proxy-support';
import { fullDepthInspectOptions } from './format-output';

/**
* Connecting text key.
Expand Down Expand Up @@ -210,10 +211,11 @@ export class CliRepl implements MongoshIOProvider {
if (jsContext === 'auto' || !jsContext) {
jsContext = willEnterInteractiveMode ? 'repl' : 'plain-vm';
}
const deepInspect = this.cliOptions.deepInspect ?? willEnterInteractiveMode;

this.mongoshRepl = new MongoshNodeRepl({
...options,
shellCliOptions: { ...this.cliOptions, jsContext, quiet },
shellCliOptions: { ...this.cliOptions, jsContext, quiet, deepInspect },
nodeReplOptions: options.nodeReplOptions ?? {
terminal: process.env.MONGOSH_FORCE_TERMINAL ? true : undefined,
},
Expand Down Expand Up @@ -738,7 +740,12 @@ export class CliRepl implements MongoshIOProvider {
formattedResult = formatForJSONOutput(e, this.cliOptions.json);
}
} else {
formattedResult = this.mongoshRepl.writer(lastEvalResult);
formattedResult = this.mongoshRepl.writer(
lastEvalResult,
this.cliOptions.deepInspect !== false
? fullDepthInspectOptions
: undefined
);
}
this.output.write(formattedResult + '\n');
}
Expand Down
3 changes: 3 additions & 0 deletions packages/cli-repl/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export const USAGE = `
--retryWrites[=true|false] ${i18n.__(
'cli-repl.args.retryWrites'
)}
--deep-inspect[=true|false] ${i18n.__(
'cli-repl.args.deepInspect'
)}

${clr(
i18n.__('cli-repl.args.authenticationOptions'),
Expand Down
5 changes: 4 additions & 1 deletion packages/cli-repl/src/format-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const CONTROL_CHAR_REGEXP_ALLOW_SIMPLE =
// eslint-disable-next-line no-control-regex
/[\x00-\x08\x0B-\x1F\x7F-\x9F]/;

const fullDepthInspectOptions = {
export const fullDepthInspectOptions = {
depth: Infinity,
maxArrayLength: Infinity,
maxStringLength: Infinity,
Expand Down Expand Up @@ -428,6 +428,7 @@ function dateInspect(
function inspect(output: unknown, options: FormatOptions): string {
// Set a custom inspection function for 'Date' objects. Since we only want this
// to affect mongosh scripts, we unset it later.
const originalDateInspect = (Date.prototype as any)[util.inspect.custom];
(Date.prototype as any)[util.inspect.custom] = dateInspect;
try {
return util.inspect(
Expand All @@ -443,6 +444,8 @@ function inspect(output: unknown, options: FormatOptions): string {
);
} finally {
delete (Date.prototype as any)[util.inspect.custom];
if (originalDateInspect)
(Date.prototype as any)[util.inspect.custom] = originalDateInspect;
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/cli-repl/src/mongosh-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import path from 'path';
import type { Duplex } from 'stream';
import { PassThrough } from 'stream';
import type { StubbedInstance } from 'ts-sinon';
import { stubInterface } from 'ts-sinon';
import sinon, { stubInterface } from 'ts-sinon';
import { inspect, promisify } from 'util';
import {
expect,
Expand Down Expand Up @@ -95,6 +95,7 @@ describe('MongoshNodeRepl', function () {
},
});
sp.runCommandWithCheck.resolves({ ok: 1 });
sp.find.resolves(sinon.stub());

if (process.env.USE_NEW_AUTOCOMPLETE) {
sp.listCollections.resolves([{ name: 'coll' }]);
Expand Down
5 changes: 3 additions & 2 deletions packages/cli-repl/src/mongosh-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ class MongoshNodeRepl implements EvaluationListener {
/**
* Format the result to a string so it can be written to the output stream.
*/
writer(result: any): string {
writer(result: any, extraFormatOptions?: Partial<FormatOptions>): string {
// This checks for error instances.
// The writer gets called immediately by the internal `repl.eval`
// in case of errors.
Expand All @@ -976,7 +976,8 @@ class MongoshNodeRepl implements EvaluationListener {
this.rawValueToShellResult.get(result) ?? {
type: null,
printable: result,
}
},
extraFormatOptions
);
}

Expand Down
136 changes: 136 additions & 0 deletions packages/e2e-tests/test/e2e-bson.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,4 +588,140 @@ describe('BSON e2e', function () {
shell.assertNoErrors();
});
});
describe('inspect nesting depth', function () {
const deepAndNestedDefinition = `({
a: { b: { c: { d: { e: { f: { g: { h: "foundme" } } } } } } },
array: [...Array(100000).keys()].map(i => ({ num: i })),
str: 'All work and no playmakes Jack a dull boy'.repeat(4096) + 'The End'
})`;
const checkForDeepOutput = (output: string, wantFullOutput: boolean) => {
if (wantFullOutput) {
expect(output).not.to.include('[Object');
expect(output).not.to.include('more items');
expect(output).to.include('foundme');
expect(output).to.include('num: 99999');
expect(output).to.include('The End');
} else {
expect(output).to.include('[Object');
expect(output).to.include('more items');
expect(output).not.to.include('foundme');
expect(output).not.to.include('num: 99999');
expect(output).not.to.include('The End');
}
};

beforeEach(async function () {
await shell.executeLine(`use ${dbName}`);
await shell.executeLine(`deepAndNested = ${deepAndNestedDefinition}`);
await shell.executeLine(`db.coll.insertOne(deepAndNested)`);
});

it('inspects a full bson document when it is read from the server (interactive mode)', async function () {
// Deeply nested object from the server should be fully printed
const output = await shell.executeLine('db.coll.findOne()');
checkForDeepOutput(output, true);
// Same object doesn't need to be fully printed if created by the user
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

const output2 = await shell.executeLine('deepAndNested');
checkForDeepOutput(output2, false);
shell.assertNoErrors();
});

it('can explicitly disable full-depth nesting (interactive mode)', async function () {
shell.kill();
shell = this.startTestShell({
args: [await testServer.connectionString(), '--deepInspect=false'],
});
await shell.waitForPrompt();
await shell.executeLine(`use ${dbName}`);
const output = await shell.executeLine('db.coll.findOne()');
checkForDeepOutput(output, false);
shell.assertNoErrors();
});

it('does not deeply inspect objects in non-interactive mode for intermediate output', async function () {
shell.kill();
shell = this.startTestShell({
args: [
await testServer.connectionString(),
'--eval',
`use(${JSON.stringify(dbName)}); print(db.coll.findOne()); 0`,
],
});
checkForDeepOutput(await shell.waitForCleanOutput(), false);
shell = this.startTestShell({
args: [
await testServer.connectionString(),
'--eval',
`print(${deepAndNestedDefinition}); 0`,
],
});
checkForDeepOutput(await shell.waitForCleanOutput(), false);
});

it('inspect full objects in non-interactive mode for final output', async function () {
shell.kill();
shell = this.startTestShell({
args: [
await testServer.connectionString(),
'--eval',
`use(${JSON.stringify(dbName)}); db.coll.findOne();`,
],
});
checkForDeepOutput(await shell.waitForCleanOutput(), true);
shell = this.startTestShell({
args: [
await testServer.connectionString(),
'--eval',
deepAndNestedDefinition,
],
});
checkForDeepOutput(await shell.waitForCleanOutput(), true);
});

it('can explicitly disable full-depth nesting (non-interactive mode)', async function () {
shell.kill();
shell = this.startTestShell({
args: [
await testServer.connectionString(),
'--deepInspect=false',
'--eval',
`use(${JSON.stringify(dbName)}); db.coll.findOne();`,
],
});
checkForDeepOutput(await shell.waitForCleanOutput(), false);
shell = this.startTestShell({
args: [
await testServer.connectionString(),
'--deepInspect=false',
'--eval',
deepAndNestedDefinition,
],
});
checkForDeepOutput(await shell.waitForCleanOutput(), false);
});

it('can parse serverStatus back to its original form', async function () {
// Dates get special treatment but that doesn't currently apply
// to mongosh's util.inspect that's available to users
// (although maybe it should?).
await shell.executeLine(
`Date.prototype[Symbol.for('nodejs.util.inspect.custom')] = function(){ return 'ISODate("' + this.toISOString() + '")'; };`
);
// 'void 0' to avoid large output in the shell from serverStatus
await shell.executeLine(
'A = db.adminCommand({ serverStatus: 1 }); void 0'
);
await shell.executeLine('util.inspect(A)');
await shell.executeLine(`B = eval('(' + util.inspect(A) + ')'); void 0`);
shell.assertNoErrors();
const output1 = await shell.executeLineWithJSONResult('A', {
parseAsEJSON: false,
});
const output2 = await shell.executeLineWithJSONResult('B', {
parseAsEJSON: false,
});
expect(output1).to.deep.equal(output2);
shell.assertNoErrors();
});
});
});
2 changes: 1 addition & 1 deletion packages/e2e-tests/test/e2e-oidc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ describe('OIDC auth e2e', function () {

// Internal hack to get a state-share server as e.g. Compass or the VSCode extension would
let handle = await shell.executeLine(
'db.getMongo()._serviceProvider.currentClientOptions.parentState.getStateShareServer()'
'db.getMongo()._serviceProvider[Symbol.for("@@mongosh.originalServiceProvider")].currentClientOptions.parentState.getStateShareServer()'
);
// `handle` can include the next prompt when returned by `shell.executeLine()`,
// so look for the longest prefix of it that is valid JSON.
Expand Down
7 changes: 5 additions & 2 deletions packages/e2e-tests/test/test-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,14 +302,17 @@ export class TestShell {
return this._output.slice(previousOutputLength);
}

async executeLineWithJSONResult(line: string): Promise<any> {
async executeLineWithJSONResult(
line: string,
{ parseAsEJSON = true } = {}
): Promise<any> {
const output = await this.executeLine(
`">>>>>>" + EJSON.stringify(${line}, {relaxed:false}) + "<<<<<<"`
);
const matching = output.match(/>>>>>>(.+)<<<<<</)?.[1];
if (!matching)
throw new Error(`Could not parse output from line: '${output}'`);
return EJSON.parse(matching);
return (parseAsEJSON ? EJSON : JSON).parse(matching);
}

assertNoErrors(): void {
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const translations: Catalog = {
quiet: 'Silence output from the shell during the connection process',
shell: 'Run the shell after executing files',
nodb: "Don't connect to mongod on startup - no 'db address' [arg] expected",
deepInspect:
'Force full depth inspection of server results (default: true if in interactive mode)',
norc: "Will not run the '.mongoshrc.js' file on start up",
eval: 'Evaluate javascript',
json: 'Print result of --eval as Extended JSON, including errors',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{ "acknowledged": true, "insertedId": <ObjectID> }
true
[ { "_id": <ObjectID>, "a": 1, "objectId": <ObjectID>, "maxKey": {"$maxKey": 1}, "minKey": {"$minKey": 1}, "binData": {"$binary": {"base64": "MTIzNA==", "subType": "10"}}, "date": {"$date": {"$numberLong": "1355875200000"}}, "isoDate": {"$date": {"$numberLong": "1355875200000"}}, "numberInt": 24, "timestamp": {"$timestamp": {"t": 100, "i": 0}}, "undefined": null, "null": null, "uuid": <UUID> } ]
{ "acknowledged": true, "insertedId": null }
{ "acknowledged": true, "insertedId": {"$date": {"$numberLong": "1355875200000"}} }
{ "acknowledged": true, "insertedId": <UUID> }
{ "acknowledged": true, "insertedId": {"$maxKey": 1} }
{ "acknowledged": true, "insertedId": 24 }
{ "acknowledged": true, "insertedId": true }
{ "acknowledged": true, "insertedId": "string key" }
{ "acknowledged": true, "insertedId": {"$binary": {"base64": "MTIzNA==", "subType": "10"}} }
{ "acknowledged": true, "insertedId": { "document": "key" } }
7 changes: 5 additions & 2 deletions packages/service-provider-core/src/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
AutoEncryptionOptions,
Collection,
} from './all-transport-types';
import type { ConnectionExtraInfo } from './index';
import type { ConnectionExtraInfo, ServiceProvider } from './index';
import type { ReplPlatform } from './platform';
import type {
AWSEncryptionKeyOptions,
Expand Down Expand Up @@ -90,7 +90,10 @@ export default interface Admin {
* @param uri
* @param options
*/
getNewConnection(uri: string, options: MongoClientOptions): Promise<any>; // returns the ServiceProvider instance
getNewConnection(
uri: string,
options: MongoClientOptions
): Promise<ServiceProvider>;

/**
* Return the URI for the current connection, if this ServiceProvider is connected.
Expand Down
2 changes: 1 addition & 1 deletion packages/service-provider-core/src/cursors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface ServiceProviderBaseCursor<TSchema = Document> {
next(): Promise<TSchema | null>;
tryNext(): Promise<TSchema | null>;
readonly closed: boolean;
[Symbol.asyncIterator](): AsyncGenerator<TSchema, void, void>;
[Symbol.asyncIterator]?(): AsyncGenerator<TSchema, void, void>;
}

export interface ServiceProviderAbstractCursor<TSchema = Document>
Expand Down
8 changes: 3 additions & 5 deletions packages/shell-api/src/abstract-cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,9 @@ export abstract class BaseCursor<
}

async *[Symbol.asyncIterator]() {
if (
this._cursor[Symbol.asyncIterator] &&
this._canDelegateIterationToUnderlyingCursor()
) {
yield* this._cursor;
const baseIterator = this._cursor[Symbol.asyncIterator];
if (baseIterator && this._canDelegateIterationToUnderlyingCursor()) {
yield* baseIterator.call(this._cursor);
return;
}

Expand Down
Loading
Loading