From 40a87277bbc711b9768e54997e63a19e9e822ef5 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 3 Jun 2024 11:00:27 +0200 Subject: [PATCH 01/33] WIP --- packages/service-provider-core/src/admin.ts | 7 +- packages/service-provider-core/src/cursors.ts | 7 + packages/service-provider-core/src/index.ts | 1 + .../deep-inspect-service-provider-wrapper.ts | 153 ++++++++++++++++++ 4 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 packages/shell-api/src/deep-inspect-service-provider-wrapper.ts diff --git a/packages/service-provider-core/src/admin.ts b/packages/service-provider-core/src/admin.ts index ef2745b0be..95bb6be767 100644 --- a/packages/service-provider-core/src/admin.ts +++ b/packages/service-provider-core/src/admin.ts @@ -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, @@ -90,7 +90,10 @@ export default interface Admin { * @param uri * @param options */ - getNewConnection(uri: string, options: MongoClientOptions): Promise; // returns the ServiceProvider instance + getNewConnection( + uri: string, + options: MongoClientOptions + ): Promise; /** * Return the URI for the current connection, if this ServiceProvider is connected. diff --git a/packages/service-provider-core/src/cursors.ts b/packages/service-provider-core/src/cursors.ts index 126e71eefd..7baca3a539 100644 --- a/packages/service-provider-core/src/cursors.ts +++ b/packages/service-provider-core/src/cursors.ts @@ -67,3 +67,10 @@ export interface ServiceProviderChangeStream next(): Promise; readonly resumeToken: ResumeToken; } + +export type ServiceProviderAnyCursor = + | ServiceProviderAggregationCursor + | ServiceProviderFindCursor + | ServiceProviderRunCommandCursor + | ServiceProviderFindCursor + | ServiceProviderChangeStream; diff --git a/packages/service-provider-core/src/index.ts b/packages/service-provider-core/src/index.ts index 6558d1c229..f356122a06 100644 --- a/packages/service-provider-core/src/index.ts +++ b/packages/service-provider-core/src/index.ts @@ -24,6 +24,7 @@ export { ServiceProviderFindCursor, ServiceProviderRunCommandCursor, ServiceProviderChangeStream, + ServiceProviderAnyCursor, } from './cursors'; export { diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts new file mode 100644 index 0000000000..c2acc9b43a --- /dev/null +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -0,0 +1,153 @@ +import type { + ServiceProvider, + ServiceProviderAnyCursor, + ServiceProviderAbstractCursor, +} from '@mongosh/service-provider-core'; +import { ServiceProviderCore } from '@mongosh/service-provider-core'; + +export class DeepInspectServiceProviderWrapper + extends ServiceProviderCore + implements ServiceProvider +{ + _sp: ServiceProvider; + + constructor(sp: ServiceProvider) { + super(sp.bsonLibrary); + this._sp = sp; + + for (const prop of Object.keys(this)) { + if (typeof (this as any)[prop] === 'function' && !(prop in sp)) { + (this as any)[prop] = undefined; + } + } + } + + aggregate = cursorMethod('aggregate'); + aggregateDb = cursorMethod('aggregateDb'); + count = bsonMethod('count'); + estimatedDocumentCount = bsonMethod('estimatedDocumentCount'); + countDocuments = bsonMethod('countDocuments'); + distinct = bsonMethod('distinct'); + find = cursorMethod('find'); + findOneAndDelete = bsonMethod('findOneAndDelete'); + findOneAndReplace = bsonMethod('findOneAndReplace'); + findOneAndUpdate = bsonMethod('findOneAndUpdate'); + getTopology = forwardedMethod('getTopology'); + getIndexes = bsonMethod('getIndexes'); + listCollections = bsonMethod('listCollections'); + readPreferenceFromOptions = forwardedMethod('readPreferenceFromOptions'); + watch = cursorMethod('watch'); + getSearchIndexes = bsonMethod('getSearchIndexes'); + runCommand = bsonMethod('runCommand'); + runCommandWithCheck = bsonMethod('runCommandWithCheck'); + runCursorCommand = cursorMethod('runCursorCommand'); + dropDatabase = bsonMethod('dropDatabase'); + dropCollection = bsonMethod('dropCollection'); + bulkWrite = bsonMethod('bulkWrite'); + deleteMany = bsonMethod('deleteMany'); + updateMany = bsonMethod('updateMany'); + updateOne = bsonMethod('updateOne'); + deleteOne = bsonMethod('deleteOne'); + createIndexes = bsonMethod('createIndexes'); + insertMany = bsonMethod('insertMany'); + insertOne = bsonMethod('insertOne'); + replaceOne = bsonMethod('replaceOne'); + initializeBulkOp = bsonMethod('initializeBulkOp'); + createSearchIndexes = bsonMethod('createSearchIndexes'); + close = forwardedMethod('close'); + suspend = forwardedMethod('suspend'); + renameCollection = bsonMethod('renameCollection'); + dropSearchIndex = bsonMethod('dropSearchIndex'); + updateSearchIndex = bsonMethod('updateSearchIndex'); + listDatabases = bsonMethod('listDatabases'); + authenticate = bsonMethod('authenticate'); + createCollection = bsonMethod('createCollection'); + getReadPreference = forwardedMethod('getReadPreference'); + getReadConcern = forwardedMethod('getReadConcern'); + getWriteConcern = forwardedMethod('getWriteConcern'); + + get platform() { + return this._sp.platform; + } + get initialDb() { + return this._sp.initialDb; + } + getURI = forwardedMethod('getURI'); + getConnectionInfo = forwardedMethod('getConnectionInfo'); + resetConnectionOptions = forwardedMethod('resetConnectionOptions'); + startSession = forwardedMethod('startSession'); + getRawClient = forwardedMethod('getRawClient'); + createClientEncryption = forwardedMethod('createClientEncryption'); + getFleOptions = forwardedMethod('getFleOptions'); + createEncryptedCollection = forwardedMethod('createEncryptedCollection'); + + async getNewConnection( + ...args: Parameters + ): Promise { + return new DeepInspectServiceProviderWrapper( + await this._sp.getNewConnection(...args) + ); + } +} + +const cursorBsonMethods: (keyof Partial)[] = [ + 'next', + 'tryNext', + 'readBufferedDocuments', + 'toArray', + '', +]; + +type PickMethodsByReturnType = { + [k in keyof T as NonNullable extends (...args: any[]) => R + ? k + : never]: T[k]; +}; + +function cursorMethod< + K extends keyof PickMethodsByReturnType< + ServiceProvider, + ServiceProviderAnyCursor + > +>( + key: K +): ( + ...args: Parameters[K]> +) => ReturnType[K]> { + return function ( + this: ServiceProvider, + ...args: Parameters + ): ReturnType { + return this[key](...args); + }; +} + +function bsonMethod< + K extends keyof PickMethodsByReturnType +>( + key: K +): ( + ...args: Parameters[K]> +) => ReturnType[K]> { + return function ( + this: ServiceProvider, + ...args: Parameters[K]> + ): ReturnType[K]> { + return this[key](...args); + }; +} + +function forwardedMethod< + K extends keyof PickMethodsByReturnType +>( + key: K +): ( + ...args: Parameters[K]> +) => ReturnType[K]> { + return function ( + this: ServiceProvider, + ...args: Parameters[K]> + ): ReturnType[K]> { + return this[key](...args); + }; +} From 4b2991c7ea0d70b92bcf4eb9a395a6932b3e0936 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Thu, 20 Nov 2025 15:29:24 +0000 Subject: [PATCH 02/33] use DeepInspectServiceProviderWrapper, install our own inspect function on results --- .../deep-inspect-service-provider-wrapper.ts | 172 ++++++++++++++---- .../shell-api/src/shell-instance-state.ts | 9 +- 2 files changed, 144 insertions(+), 37 deletions(-) diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index c2acc9b43a..f96e27758b 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -1,9 +1,10 @@ import type { ServiceProvider, - ServiceProviderAnyCursor, ServiceProviderAbstractCursor, } from '@mongosh/service-provider-core'; import { ServiceProviderCore } from '@mongosh/service-provider-core'; +import type { InspectOptions, inspect as _inspect } from 'util'; +import type { Document } from '@mongosh/service-provider-core'; export class DeepInspectServiceProviderWrapper extends ServiceProviderCore @@ -24,26 +25,28 @@ export class DeepInspectServiceProviderWrapper aggregate = cursorMethod('aggregate'); aggregateDb = cursorMethod('aggregateDb'); - count = bsonMethod('count'); - estimatedDocumentCount = bsonMethod('estimatedDocumentCount'); - countDocuments = bsonMethod('countDocuments'); + count = forwardedMethod('count'); + estimatedDocumentCount = forwardedMethod('estimatedDocumentCount'); + countDocuments = forwardedMethod('countDocuments'); distinct = bsonMethod('distinct'); find = cursorMethod('find'); findOneAndDelete = bsonMethod('findOneAndDelete'); findOneAndReplace = bsonMethod('findOneAndReplace'); findOneAndUpdate = bsonMethod('findOneAndUpdate'); - getTopology = forwardedMethod('getTopology'); + getTopologyDescription = forwardedMethod('getTopologyDescription'); getIndexes = bsonMethod('getIndexes'); listCollections = bsonMethod('listCollections'); readPreferenceFromOptions = forwardedMethod('readPreferenceFromOptions'); - watch = cursorMethod('watch'); + // TODO: this should be a cursor method, but the types are incompatible + watch = forwardedMethod('watch'); getSearchIndexes = bsonMethod('getSearchIndexes'); runCommand = bsonMethod('runCommand'); runCommandWithCheck = bsonMethod('runCommandWithCheck'); runCursorCommand = cursorMethod('runCursorCommand'); dropDatabase = bsonMethod('dropDatabase'); - dropCollection = bsonMethod('dropCollection'); + dropCollection = forwardedMethod('dropCollection'); bulkWrite = bsonMethod('bulkWrite'); + clientBulkWrite = bsonMethod('clientBulkWrite'); deleteMany = bsonMethod('deleteMany'); updateMany = bsonMethod('updateMany'); updateOne = bsonMethod('updateOne'); @@ -53,15 +56,15 @@ export class DeepInspectServiceProviderWrapper insertOne = bsonMethod('insertOne'); replaceOne = bsonMethod('replaceOne'); initializeBulkOp = bsonMethod('initializeBulkOp'); - createSearchIndexes = bsonMethod('createSearchIndexes'); + createSearchIndexes = forwardedMethod('createSearchIndexes'); close = forwardedMethod('close'); suspend = forwardedMethod('suspend'); - renameCollection = bsonMethod('renameCollection'); - dropSearchIndex = bsonMethod('dropSearchIndex'); - updateSearchIndex = bsonMethod('updateSearchIndex'); + renameCollection = forwardedMethod('renameCollection'); + dropSearchIndex = forwardedMethod('dropSearchIndex'); + updateSearchIndex = forwardedMethod('updateSearchIndex'); listDatabases = bsonMethod('listDatabases'); - authenticate = bsonMethod('authenticate'); - createCollection = bsonMethod('createCollection'); + authenticate = forwardedMethod('authenticate'); + createCollection = forwardedMethod('createCollection'); getReadPreference = forwardedMethod('getReadPreference'); getReadConcern = forwardedMethod('getReadConcern'); getWriteConcern = forwardedMethod('getWriteConcern'); @@ -72,6 +75,7 @@ export class DeepInspectServiceProviderWrapper get initialDb() { return this._sp.initialDb; } + getURI = forwardedMethod('getURI'); getConnectionInfo = forwardedMethod('getConnectionInfo'); resetConnectionOptions = forwardedMethod('resetConnectionOptions'); @@ -84,20 +88,11 @@ export class DeepInspectServiceProviderWrapper async getNewConnection( ...args: Parameters ): Promise { - return new DeepInspectServiceProviderWrapper( - await this._sp.getNewConnection(...args) - ); + const sp = await this._sp.getNewConnection(...args); + return new DeepInspectServiceProviderWrapper(sp as ServiceProvider); } } -const cursorBsonMethods: (keyof Partial)[] = [ - 'next', - 'tryNext', - 'readBufferedDocuments', - 'toArray', - '', -]; - type PickMethodsByReturnType = { [k in keyof T as NonNullable extends (...args: any[]) => R ? k @@ -107,7 +102,7 @@ type PickMethodsByReturnType = { function cursorMethod< K extends keyof PickMethodsByReturnType< ServiceProvider, - ServiceProviderAnyCursor + ServiceProviderAbstractCursor > >( key: K @@ -115,25 +110,96 @@ function cursorMethod< ...args: Parameters[K]> ) => ReturnType[K]> { return function ( - this: ServiceProvider, + this: DeepInspectServiceProviderWrapper, ...args: Parameters ): ReturnType { - return this[key](...args); + // The problem here is that ReturnType results in + // ServiceProviderAnyCursor which includes ServiceProviderChangeStream which + // doesn't have readBufferedDocuments or toArray. We can try cast things to + // ServiceProviderAbstractCursor, but then that's not assignable to + // ServiceProviderAnyCursor. And that's why there's so much casting below. + const cursor = (this._sp[key] as any)(...args) as any; + + cursor.next = cursorNext( + cursor.next.bind(cursor) as () => Promise + ); + cursor.tryNext = cursorTryNext( + cursor.tryNext.bind(cursor) as () => Promise + ); + + if (cursor.readBufferedDocuments) { + cursor.readBufferedDocuments = cursorReadBufferedDocuments( + cursor.readBufferedDocuments.bind(cursor) as ( + number?: number + ) => Document[] + ); + } + if (cursor.toArray) { + cursor.toArray = cursorToArray( + cursor.toArray.bind(cursor) as () => Promise + ); + } + + return cursor; + }; +} + +const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); + +function cursorNext( + original: () => Promise +): () => Promise { + return async function (): Promise { + const result = await original(); + if (result) { + replaceWithCustomInspect(result); + } + return result; + }; +} + +const cursorTryNext = cursorNext; + +function cursorReadBufferedDocuments( + original: (number?: number) => Document[] +): (number?: number) => Document[] { + return function (number?: number): Document[] { + const results = original(number); + + replaceWithCustomInspect(results); + + return results; + }; +} + +function cursorToArray( + original: () => Promise +): () => Promise { + return async function (): Promise { + const results = await original(); + + replaceWithCustomInspect(results); + + return results; }; } function bsonMethod< - K extends keyof PickMethodsByReturnType + K extends keyof PickMethodsByReturnType> >( key: K ): ( ...args: Parameters[K]> ) => ReturnType[K]> { - return function ( - this: ServiceProvider, + return async function ( + this: DeepInspectServiceProviderWrapper, ...args: Parameters[K]> - ): ReturnType[K]> { - return this[key](...args); + ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore The returntype already contains a promise + ReturnType[K]> { + const result = await (this._sp[key] as any)(...args); + replaceWithCustomInspect(result); + return result; }; } @@ -145,9 +211,47 @@ function forwardedMethod< ...args: Parameters[K]> ) => ReturnType[K]> { return function ( - this: ServiceProvider, + this: DeepInspectServiceProviderWrapper, ...args: Parameters[K]> ): ReturnType[K]> { - return this[key](...args); + // not wrapping the result at all because forwardedMethod() is for simple + // values only + return (this._sp[key] as any)(...args); }; } + +function customDocumentInspect( + this: Document, + depth: number, + inspectOptions: InspectOptions, + inspect: typeof _inspect +) { + const newInspectOptions = { + ...inspectOptions, + depth: Infinity, + maxArrayLength: Infinity, + maxStringLength: Infinity, + }; + + // reuse the standard inpect logic for an object without causing infinite + // recursion + const inspectBackup = (this as any)[customInspectSymbol]; + delete (this as any)[customInspectSymbol]; + const result = inspect(this, newInspectOptions); + (this as any)[customInspectSymbol] = inspectBackup; + return result; +} + +function replaceWithCustomInspect(obj: any) { + if (Array.isArray(obj)) { + (obj as any)[customInspectSymbol] = customDocumentInspect; + for (const item of obj) { + replaceWithCustomInspect(item); + } + } else if (obj && typeof obj === 'object' && obj !== null) { + obj[customInspectSymbol] = customDocumentInspect; + for (const value of Object.values(obj)) { + replaceWithCustomInspect(value); + } + } +} diff --git a/packages/shell-api/src/shell-instance-state.ts b/packages/shell-api/src/shell-instance-state.ts index f2f402f4a1..9a98824c95 100644 --- a/packages/shell-api/src/shell-instance-state.ts +++ b/packages/shell-api/src/shell-instance-state.ts @@ -51,6 +51,7 @@ import type { AutocompletionContext } from '@mongodb-js/mongodb-ts-autocomplete' import type { JSONSchema } from 'mongodb-schema'; import { analyzeDocuments } from 'mongodb-schema'; import type { BaseCursor } from './abstract-cursor'; +import { DeepInspectServiceProviderWrapper } from './deep-inspect-service-provider-wrapper'; /** * The subset of CLI options that is relevant for the shell API's behavior itself. @@ -203,7 +204,9 @@ export class ShellInstanceState { cliOptions: ShellCliOptions = {}, bsonLibrary: BSONLibrary = initialServiceProvider.bsonLibrary ) { - this.initialServiceProvider = initialServiceProvider; + this.initialServiceProvider = new DeepInspectServiceProviderWrapper( + initialServiceProvider + ); this.bsonLibrary = bsonLibrary; this.messageBus = messageBus; this.shellApi = new ShellApi(this); @@ -220,11 +223,11 @@ export class ShellInstanceState { undefined, undefined, undefined, - initialServiceProvider + this.initialServiceProvider ); this.mongos.push(mongo); this.currentDb = mongo.getDB( - initialServiceProvider.initialDb || DEFAULT_DB + this.initialServiceProvider.initialDb || DEFAULT_DB ); } else { this.currentDb = new NoDatabase() as DatabaseWithSchema; From f85dc9faf99d534b277c1fa5d45f74668d439589 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Thu, 20 Nov 2025 16:50:26 +0000 Subject: [PATCH 03/33] you cannot extend the return value of initializeBulkOp --- packages/shell-api/src/deep-inspect-service-provider-wrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index f96e27758b..538fe1b71f 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -55,7 +55,7 @@ export class DeepInspectServiceProviderWrapper insertMany = bsonMethod('insertMany'); insertOne = bsonMethod('insertOne'); replaceOne = bsonMethod('replaceOne'); - initializeBulkOp = bsonMethod('initializeBulkOp'); + initializeBulkOp = forwardedMethod('initializeBulkOp'); // you cannot extend the return value here createSearchIndexes = forwardedMethod('createSearchIndexes'); close = forwardedMethod('close'); suspend = forwardedMethod('suspend'); From 24ef0432604939d7e43a5c83c19b77a6db89d4be Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Thu, 20 Nov 2025 16:52:00 +0000 Subject: [PATCH 04/33] apparently inspect passed as the final parameter isn't always a thing --- .../shell-api/src/deep-inspect-service-provider-wrapper.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index 538fe1b71f..523ee77234 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -3,7 +3,8 @@ import type { ServiceProviderAbstractCursor, } from '@mongosh/service-provider-core'; import { ServiceProviderCore } from '@mongosh/service-provider-core'; -import type { InspectOptions, inspect as _inspect } from 'util'; +import type { InspectOptions } from 'util'; +import { inspect } from 'util'; import type { Document } from '@mongosh/service-provider-core'; export class DeepInspectServiceProviderWrapper @@ -223,8 +224,7 @@ function forwardedMethod< function customDocumentInspect( this: Document, depth: number, - inspectOptions: InspectOptions, - inspect: typeof _inspect + inspectOptions: InspectOptions ) { const newInspectOptions = { ...inspectOptions, From cda077c6b6664f98b9b5286bed037af0f858c6ca Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 24 Nov 2025 10:01:44 +0000 Subject: [PATCH 05/33] better name --- .../src/deep-inspect-service-provider-wrapper.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index 523ee77234..47d4b535a6 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -153,7 +153,7 @@ function cursorNext( return async function (): Promise { const result = await original(); if (result) { - replaceWithCustomInspect(result); + addCustomInspect(result); } return result; }; @@ -167,7 +167,7 @@ function cursorReadBufferedDocuments( return function (number?: number): Document[] { const results = original(number); - replaceWithCustomInspect(results); + addCustomInspect(results); return results; }; @@ -179,7 +179,7 @@ function cursorToArray( return async function (): Promise { const results = await original(); - replaceWithCustomInspect(results); + addCustomInspect(results); return results; }; @@ -199,7 +199,7 @@ function bsonMethod< // @ts-ignore The returntype already contains a promise ReturnType[K]> { const result = await (this._sp[key] as any)(...args); - replaceWithCustomInspect(result); + addCustomInspect(result); return result; }; } @@ -242,16 +242,16 @@ function customDocumentInspect( return result; } -function replaceWithCustomInspect(obj: any) { +function addCustomInspect(obj: any) { if (Array.isArray(obj)) { (obj as any)[customInspectSymbol] = customDocumentInspect; for (const item of obj) { - replaceWithCustomInspect(item); + addCustomInspect(item); } } else if (obj && typeof obj === 'object' && obj !== null) { obj[customInspectSymbol] = customDocumentInspect; for (const value of Object.values(obj)) { - replaceWithCustomInspect(value); + addCustomInspect(value); } } } From 7d008f7c1f910e9cd5db6d8369712edbe76f56e6 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 24 Nov 2025 10:07:14 +0000 Subject: [PATCH 06/33] inspect a shallow copy --- .../src/deep-inspect-service-provider-wrapper.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index 47d4b535a6..8d4d5bbe3f 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -234,12 +234,9 @@ function customDocumentInspect( }; // reuse the standard inpect logic for an object without causing infinite - // recursion - const inspectBackup = (this as any)[customInspectSymbol]; - delete (this as any)[customInspectSymbol]; - const result = inspect(this, newInspectOptions); - (this as any)[customInspectSymbol] = inspectBackup; - return result; + const copyToInspect: any = Array.isArray(this) ? this.slice() : { ...this }; + delete copyToInspect[customInspectSymbol]; + return inspect(copyToInspect, newInspectOptions); } function addCustomInspect(obj: any) { From a27b349c35ba50ac77596c2a9c40056c264557e3 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 24 Nov 2025 10:07:51 +0000 Subject: [PATCH 07/33] fix comment --- packages/shell-api/src/deep-inspect-service-provider-wrapper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index 8d4d5bbe3f..73a9b301cb 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -234,6 +234,7 @@ function customDocumentInspect( }; // reuse the standard inpect logic for an object without causing infinite + // recursion const copyToInspect: any = Array.isArray(this) ? this.slice() : { ...this }; delete copyToInspect[customInspectSymbol]; return inspect(copyToInspect, newInspectOptions); From 8c523bb4182be4bb2fe4bcaf73688efaa8515dc5 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 24 Nov 2025 10:11:01 +0000 Subject: [PATCH 08/33] leave bson values alone, don't accidentally override existing custom inspect symbols --- .../shell-api/src/deep-inspect-service-provider-wrapper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index 73a9b301cb..1dd75737cf 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -242,11 +242,11 @@ function customDocumentInspect( function addCustomInspect(obj: any) { if (Array.isArray(obj)) { - (obj as any)[customInspectSymbol] = customDocumentInspect; + (obj as any)[customInspectSymbol] ??= customDocumentInspect; for (const item of obj) { addCustomInspect(item); } - } else if (obj && typeof obj === 'object' && obj !== null) { + } else if (obj && typeof obj === 'object' && obj !== null && !obj._bsontype) { obj[customInspectSymbol] = customDocumentInspect; for (const value of Object.values(obj)) { addCustomInspect(value); From aa2e83010ddf792ae2374463429f35e4c5ff9ba0 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 24 Nov 2025 10:54:59 +0000 Subject: [PATCH 09/33] don't remove things --- .../shell-api/src/deep-inspect-service-provider-wrapper.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index 1dd75737cf..3ab7a0a258 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -16,12 +16,6 @@ export class DeepInspectServiceProviderWrapper constructor(sp: ServiceProvider) { super(sp.bsonLibrary); this._sp = sp; - - for (const prop of Object.keys(this)) { - if (typeof (this as any)[prop] === 'function' && !(prop in sp)) { - (this as any)[prop] = undefined; - } - } } aggregate = cursorMethod('aggregate'); From 6307dd356a18383b333cf6323ff7953db2fabf38 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 24 Nov 2025 12:53:38 +0000 Subject: [PATCH 10/33] wrap every type of cursor --- packages/service-provider-core/src/cursors.ts | 7 - packages/service-provider-core/src/index.ts | 1 - packages/shell-api/src/custom-inspect.ts | 37 ++++ ...deep-inspect-aggregation-cursor-wrapper.ts | 142 +++++++++++++++ .../src/deep-inspect-change-stream-wrapper.ts | 80 +++++++++ .../src/deep-inspect-find-cursor-wrapper.ts | 156 ++++++++++++++++ ...deep-inspect-run-command-cursor-wrapper.ts | 127 ++++++++++++++ .../deep-inspect-service-provider-wrapper.ts | 166 +++--------------- .../src/pick-methods-by-return-type.ts | 5 + 9 files changed, 576 insertions(+), 145 deletions(-) create mode 100644 packages/shell-api/src/custom-inspect.ts create mode 100644 packages/shell-api/src/deep-inspect-aggregation-cursor-wrapper.ts create mode 100644 packages/shell-api/src/deep-inspect-change-stream-wrapper.ts create mode 100644 packages/shell-api/src/deep-inspect-find-cursor-wrapper.ts create mode 100644 packages/shell-api/src/deep-inspect-run-command-cursor-wrapper.ts create mode 100644 packages/shell-api/src/pick-methods-by-return-type.ts diff --git a/packages/service-provider-core/src/cursors.ts b/packages/service-provider-core/src/cursors.ts index 7baca3a539..126e71eefd 100644 --- a/packages/service-provider-core/src/cursors.ts +++ b/packages/service-provider-core/src/cursors.ts @@ -67,10 +67,3 @@ export interface ServiceProviderChangeStream next(): Promise; readonly resumeToken: ResumeToken; } - -export type ServiceProviderAnyCursor = - | ServiceProviderAggregationCursor - | ServiceProviderFindCursor - | ServiceProviderRunCommandCursor - | ServiceProviderFindCursor - | ServiceProviderChangeStream; diff --git a/packages/service-provider-core/src/index.ts b/packages/service-provider-core/src/index.ts index f356122a06..6558d1c229 100644 --- a/packages/service-provider-core/src/index.ts +++ b/packages/service-provider-core/src/index.ts @@ -24,7 +24,6 @@ export { ServiceProviderFindCursor, ServiceProviderRunCommandCursor, ServiceProviderChangeStream, - ServiceProviderAnyCursor, } from './cursors'; export { diff --git a/packages/shell-api/src/custom-inspect.ts b/packages/shell-api/src/custom-inspect.ts new file mode 100644 index 0000000000..7691c8e7fd --- /dev/null +++ b/packages/shell-api/src/custom-inspect.ts @@ -0,0 +1,37 @@ +import { inspect } from 'util'; +import type { InspectOptions } from 'util'; + +const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); + +function customDocumentInspect( + this: Document, + depth: number, + inspectOptions: InspectOptions +) { + const newInspectOptions = { + ...inspectOptions, + depth: Infinity, + maxArrayLength: Infinity, + maxStringLength: Infinity, + }; + + // reuse the standard inpect logic for an object without causing infinite + // recursion + const copyToInspect: any = Array.isArray(this) ? this.slice() : { ...this }; + delete copyToInspect[customInspectSymbol]; + return inspect(copyToInspect, newInspectOptions); +} + +export function addCustomInspect(obj: any) { + if (Array.isArray(obj)) { + (obj as any)[customInspectSymbol] ??= customDocumentInspect; + for (const item of obj) { + addCustomInspect(item); + } + } else if (obj && typeof obj === 'object' && obj !== null && !obj._bsontype) { + obj[customInspectSymbol] = customDocumentInspect; + for (const value of Object.values(obj)) { + addCustomInspect(value); + } + } +} diff --git a/packages/shell-api/src/deep-inspect-aggregation-cursor-wrapper.ts b/packages/shell-api/src/deep-inspect-aggregation-cursor-wrapper.ts new file mode 100644 index 0000000000..6812dd9084 --- /dev/null +++ b/packages/shell-api/src/deep-inspect-aggregation-cursor-wrapper.ts @@ -0,0 +1,142 @@ +import type { + Document, + ReadConcernLike, + ReadPreferenceLike, + ServiceProviderAggregationCursor, +} from '@mongosh/service-provider-core'; +import type { PickMethodsByReturnType } from './pick-methods-by-return-type'; +import { addCustomInspect } from './custom-inspect'; + +export class DeepInspectAggregationCursorWrapper + implements ServiceProviderAggregationCursor +{ + _cursor: ServiceProviderAggregationCursor; + + constructor(cursor: ServiceProviderAggregationCursor) { + this._cursor = cursor; + } + + project = forwardedMethod('project'); + skip = forwardedMethod('skip'); + sort = forwardedMethod('sort'); + explain = forwardedMethod('explain'); + addCursorFlag = forwardedMethod('addCursorFlag'); + withReadPreference = (readPreference: ReadPreferenceLike) => { + this._cursor.withReadPreference(readPreference); + return this; + }; + withReadConcern(readConcern: ReadConcernLike) { + this._cursor.withReadConcern(readConcern); + return this; + } + batchSize = forwardedMethod('batchSize'); + hasNext = forwardedMethod('hasNext'); + close = forwardedMethod('close'); + maxTimeMS = forwardedMethod('maxTimeMS'); + bufferedCount = forwardedMethod('bufferedCount'); + + next = forwardResultPromise('next'); + tryNext = forwardResultPromise('tryNext'); + + toArray = forwardResultsPromise('toArray'); + readBufferedDocuments = forwardResults( + 'readBufferedDocuments' + ); + + get closed(): boolean { + return this._cursor.closed; + } + + async *[Symbol.asyncIterator]() { + yield* this._cursor; + return; + } +} + +function forwardResultPromise< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderAggregationCursor, + Promise + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return async function ( + this: DeepInspectAggregationCursorWrapper, + ...args: Parameters>[K]> + ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore The returntype already contains a promise + ReturnType>[K]> { + const result = await (this._cursor[key] as any)(...args); + if (result) { + addCustomInspect(result); + } + return result; + }; +} + +function forwardResultsPromise< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderAggregationCursor, + Promise + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return async function ( + this: DeepInspectAggregationCursorWrapper, + ...args: Parameters>[K]> + ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore The returntype already contains a promise + ReturnType>[K]> { + const results = await (this._cursor[key] as any)(...args); + addCustomInspect(results); + return results; + }; +} + +function forwardResults< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderAggregationCursor, + TSchema[] + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return function ( + this: DeepInspectAggregationCursorWrapper, + ...args: Parameters>[K]> + ): ReturnType>[K]> { + const results = (this._cursor[key] as any)(...args); + addCustomInspect(results); + return results; + }; +} + +function forwardedMethod< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderAggregationCursor, + any + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return function ( + this: DeepInspectAggregationCursorWrapper, + ...args: Parameters>[K]> + ): ReturnType>[K]> { + return (this._cursor[key] as any)(...args); + }; +} diff --git a/packages/shell-api/src/deep-inspect-change-stream-wrapper.ts b/packages/shell-api/src/deep-inspect-change-stream-wrapper.ts new file mode 100644 index 0000000000..21b547488b --- /dev/null +++ b/packages/shell-api/src/deep-inspect-change-stream-wrapper.ts @@ -0,0 +1,80 @@ +import type { + Document, + ResumeToken, + ServiceProviderChangeStream, +} from '@mongosh/service-provider-core'; +import type { PickMethodsByReturnType } from './pick-methods-by-return-type'; +import { addCustomInspect } from './custom-inspect'; + +export class DeepInspectChangeStreamWrapper + implements ServiceProviderChangeStream +{ + _cursor: ServiceProviderChangeStream; + + constructor(cursor: ServiceProviderChangeStream) { + this._cursor = cursor; + } + + get resumeToken(): ResumeToken { + return this._cursor.resumeToken; + } + + hasNext = forwardedMethod('hasNext'); + close = forwardedMethod('close'); + + next = forwardResultPromise('next'); + tryNext = forwardResultPromise('tryNext'); + + get closed(): boolean { + return this._cursor.closed; + } + + async *[Symbol.asyncIterator]() { + yield* this._cursor; + return; + } +} + +function forwardResultPromise< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderChangeStream, + Promise + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return async function ( + this: DeepInspectChangeStreamWrapper, + ...args: Parameters>[K]> + ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore The returntype already contains a promise + ReturnType>[K]> { + const result = await (this._cursor[key] as any)(...args); + if (result) { + addCustomInspect(result); + } + return result; + }; +} + +function forwardedMethod< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderChangeStream, + any + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return function ( + this: DeepInspectChangeStreamWrapper, + ...args: Parameters>[K]> + ): ReturnType>[K]> { + return (this._cursor[key] as any)(...args); + }; +} diff --git a/packages/shell-api/src/deep-inspect-find-cursor-wrapper.ts b/packages/shell-api/src/deep-inspect-find-cursor-wrapper.ts new file mode 100644 index 0000000000..f008d45879 --- /dev/null +++ b/packages/shell-api/src/deep-inspect-find-cursor-wrapper.ts @@ -0,0 +1,156 @@ +import type { + Document, + ReadConcernLike, + ReadPreferenceLike, + ServiceProviderFindCursor, +} from '@mongosh/service-provider-core'; +import type { PickMethodsByReturnType } from './pick-methods-by-return-type'; +import { addCustomInspect } from './custom-inspect'; + +export class DeepInspectFindCursorWrapper + implements ServiceProviderFindCursor +{ + _cursor: ServiceProviderFindCursor; + + constructor(cursor: ServiceProviderFindCursor) { + this._cursor = cursor; + } + + allowDiskUse = forwardedMethod('allowDiskUse'); + collation = forwardedMethod('collation'); + comment = forwardedMethod('comment'); + maxAwaitTimeMS = forwardedMethod('maxAwaitTimeMS'); + count = forwardedMethod('count'); + hint = forwardedMethod('hint'); + max = forwardedMethod('max'); + min = forwardedMethod('min'); + limit = forwardedMethod('limit'); + skip = forwardedMethod('skip'); + returnKey = forwardedMethod('returnKey'); + showRecordId = forwardedMethod('showRecordId'); + project = forwardedMethod('project'); + sort = forwardedMethod('sort'); + explain = forwardedMethod('explain'); + addCursorFlag = forwardedMethod('addCursorFlag'); + + withReadPreference = (readPreference: ReadPreferenceLike) => { + this._cursor.withReadPreference(readPreference); + return this; + }; + + withReadConcern(readConcern: ReadConcernLike) { + this._cursor.withReadConcern(readConcern); + return this; + } + + batchSize = forwardedMethod('batchSize'); + hasNext = forwardedMethod('hasNext'); + close = forwardedMethod('close'); + maxTimeMS = forwardedMethod('maxTimeMS'); + bufferedCount = forwardedMethod('bufferedCount'); + + next = forwardResultPromise('next'); + tryNext = forwardResultPromise('tryNext'); + + toArray = forwardResultsPromise('toArray'); + readBufferedDocuments = forwardResults( + 'readBufferedDocuments' + ); + + get closed(): boolean { + return this._cursor.closed; + } + + async *[Symbol.asyncIterator]() { + yield* this._cursor; + return; + } +} + +function forwardResultPromise< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderFindCursor, + Promise + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return async function ( + this: DeepInspectFindCursorWrapper, + ...args: Parameters>[K]> + ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore The returntype already contains a promise + ReturnType>[K]> { + const result = await (this._cursor[key] as any)(...args); + if (result) { + addCustomInspect(result); + } + return result; + }; +} + +function forwardResultsPromise< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderFindCursor, + Promise + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return async function ( + this: DeepInspectFindCursorWrapper, + ...args: Parameters>[K]> + ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore The returntype already contains a promise + ReturnType>[K]> { + const results = await (this._cursor[key] as any)(...args); + addCustomInspect(results); + return results; + }; +} + +function forwardResults< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderFindCursor, + TSchema[] + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return function ( + this: DeepInspectFindCursorWrapper, + ...args: Parameters>[K]> + ): ReturnType>[K]> { + const results = (this._cursor[key] as any)(...args); + addCustomInspect(results); + return results; + }; +} + +function forwardedMethod< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderFindCursor, + any + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return function ( + this: DeepInspectFindCursorWrapper, + ...args: Parameters>[K]> + ): ReturnType>[K]> { + return (this._cursor[key] as any)(...args); + }; +} diff --git a/packages/shell-api/src/deep-inspect-run-command-cursor-wrapper.ts b/packages/shell-api/src/deep-inspect-run-command-cursor-wrapper.ts new file mode 100644 index 0000000000..a918f4ae3a --- /dev/null +++ b/packages/shell-api/src/deep-inspect-run-command-cursor-wrapper.ts @@ -0,0 +1,127 @@ +import type { + Document, + ServiceProviderRunCommandCursor, +} from '@mongosh/service-provider-core'; +import type { PickMethodsByReturnType } from './pick-methods-by-return-type'; +import { addCustomInspect } from './custom-inspect'; + +export class DeepInspectRunCommandCursorWrapper + implements ServiceProviderRunCommandCursor +{ + _cursor: ServiceProviderRunCommandCursor; + + constructor(cursor: ServiceProviderRunCommandCursor) { + this._cursor = cursor; + } + + batchSize = forwardedMethod('batchSize'); + hasNext = forwardedMethod('hasNext'); + close = forwardedMethod('close'); + maxTimeMS = forwardedMethod('maxTimeMS'); + bufferedCount = forwardedMethod('bufferedCount'); + + next = forwardResultPromise('next'); + tryNext = forwardResultPromise('tryNext'); + + toArray = forwardResultsPromise('toArray'); + readBufferedDocuments = forwardResults( + 'readBufferedDocuments' + ); + + get closed(): boolean { + return this._cursor.closed; + } + + async *[Symbol.asyncIterator]() { + yield* this._cursor; + return; + } +} + +function forwardResultPromise< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderRunCommandCursor, + Promise + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return async function ( + this: DeepInspectRunCommandCursorWrapper, + ...args: Parameters>[K]> + ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore The returntype already contains a promise + ReturnType>[K]> { + const result = await (this._cursor[key] as any)(...args); + if (result) { + addCustomInspect(result); + } + return result; + }; +} + +function forwardResultsPromise< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderRunCommandCursor, + Promise + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return async function ( + this: DeepInspectRunCommandCursorWrapper, + ...args: Parameters>[K]> + ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore The returntype already contains a promise + ReturnType>[K]> { + const results = await (this._cursor[key] as any)(...args); + addCustomInspect(results); + return results; + }; +} + +function forwardResults< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderRunCommandCursor, + TSchema[] + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return function ( + this: DeepInspectRunCommandCursorWrapper, + ...args: Parameters>[K]> + ): ReturnType>[K]> { + const results = (this._cursor[key] as any)(...args); + addCustomInspect(results); + return results; + }; +} + +function forwardedMethod< + TSchema, + K extends keyof PickMethodsByReturnType< + ServiceProviderRunCommandCursor, + any + > +>( + key: K +): ( + ...args: Parameters>[K]> +) => ReturnType>[K]> { + return function ( + this: DeepInspectRunCommandCursorWrapper, + ...args: Parameters>[K]> + ): ReturnType>[K]> { + return (this._cursor[key] as any)(...args); + }; +} diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index 3ab7a0a258..bfda4fd83b 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -1,11 +1,11 @@ -import type { - ServiceProvider, - ServiceProviderAbstractCursor, -} from '@mongosh/service-provider-core'; +import type { ServiceProvider } from '@mongosh/service-provider-core'; import { ServiceProviderCore } from '@mongosh/service-provider-core'; -import type { InspectOptions } from 'util'; -import { inspect } from 'util'; -import type { Document } from '@mongosh/service-provider-core'; +import { DeepInspectAggregationCursorWrapper } from './deep-inspect-aggregation-cursor-wrapper'; +import { DeepInspectFindCursorWrapper } from './deep-inspect-find-cursor-wrapper'; +import { addCustomInspect } from './custom-inspect'; +import type { PickMethodsByReturnType } from './pick-methods-by-return-type'; +import { DeepInspectRunCommandCursorWrapper } from './deep-inspect-run-command-cursor-wrapper'; +import { DeepInspectChangeStreamWrapper } from './deep-inspect-change-stream-wrapper'; export class DeepInspectServiceProviderWrapper extends ServiceProviderCore @@ -18,13 +18,22 @@ export class DeepInspectServiceProviderWrapper this._sp = sp; } - aggregate = cursorMethod('aggregate'); - aggregateDb = cursorMethod('aggregateDb'); + aggregate = (...args: Parameters) => { + const cursor = this._sp.aggregate(...args); + return new DeepInspectAggregationCursorWrapper(cursor); + }; + aggregateDb = (...args: Parameters) => { + const cursor = this._sp.aggregateDb(...args); + return new DeepInspectAggregationCursorWrapper(cursor); + }; count = forwardedMethod('count'); estimatedDocumentCount = forwardedMethod('estimatedDocumentCount'); countDocuments = forwardedMethod('countDocuments'); distinct = bsonMethod('distinct'); - find = cursorMethod('find'); + find = (...args: Parameters) => { + const cursor = this._sp.find(...args); + return new DeepInspectFindCursorWrapper(cursor); + }; findOneAndDelete = bsonMethod('findOneAndDelete'); findOneAndReplace = bsonMethod('findOneAndReplace'); findOneAndUpdate = bsonMethod('findOneAndUpdate'); @@ -32,12 +41,19 @@ export class DeepInspectServiceProviderWrapper getIndexes = bsonMethod('getIndexes'); listCollections = bsonMethod('listCollections'); readPreferenceFromOptions = forwardedMethod('readPreferenceFromOptions'); - // TODO: this should be a cursor method, but the types are incompatible - watch = forwardedMethod('watch'); + watch = (...args: Parameters) => { + const cursor = this._sp.watch(...args); + return new DeepInspectChangeStreamWrapper(cursor); + }; getSearchIndexes = bsonMethod('getSearchIndexes'); runCommand = bsonMethod('runCommand'); runCommandWithCheck = bsonMethod('runCommandWithCheck'); - runCursorCommand = cursorMethod('runCursorCommand'); + runCursorCommand = ( + ...args: Parameters + ) => { + const cursor = this._sp.runCursorCommand(...args); + return new DeepInspectRunCommandCursorWrapper(cursor); + }; dropDatabase = bsonMethod('dropDatabase'); dropCollection = forwardedMethod('dropCollection'); bulkWrite = bsonMethod('bulkWrite'); @@ -88,97 +104,6 @@ export class DeepInspectServiceProviderWrapper } } -type PickMethodsByReturnType = { - [k in keyof T as NonNullable extends (...args: any[]) => R - ? k - : never]: T[k]; -}; - -function cursorMethod< - K extends keyof PickMethodsByReturnType< - ServiceProvider, - ServiceProviderAbstractCursor - > ->( - key: K -): ( - ...args: Parameters[K]> -) => ReturnType[K]> { - return function ( - this: DeepInspectServiceProviderWrapper, - ...args: Parameters - ): ReturnType { - // The problem here is that ReturnType results in - // ServiceProviderAnyCursor which includes ServiceProviderChangeStream which - // doesn't have readBufferedDocuments or toArray. We can try cast things to - // ServiceProviderAbstractCursor, but then that's not assignable to - // ServiceProviderAnyCursor. And that's why there's so much casting below. - const cursor = (this._sp[key] as any)(...args) as any; - - cursor.next = cursorNext( - cursor.next.bind(cursor) as () => Promise - ); - cursor.tryNext = cursorTryNext( - cursor.tryNext.bind(cursor) as () => Promise - ); - - if (cursor.readBufferedDocuments) { - cursor.readBufferedDocuments = cursorReadBufferedDocuments( - cursor.readBufferedDocuments.bind(cursor) as ( - number?: number - ) => Document[] - ); - } - if (cursor.toArray) { - cursor.toArray = cursorToArray( - cursor.toArray.bind(cursor) as () => Promise - ); - } - - return cursor; - }; -} - -const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); - -function cursorNext( - original: () => Promise -): () => Promise { - return async function (): Promise { - const result = await original(); - if (result) { - addCustomInspect(result); - } - return result; - }; -} - -const cursorTryNext = cursorNext; - -function cursorReadBufferedDocuments( - original: (number?: number) => Document[] -): (number?: number) => Document[] { - return function (number?: number): Document[] { - const results = original(number); - - addCustomInspect(results); - - return results; - }; -} - -function cursorToArray( - original: () => Promise -): () => Promise { - return async function (): Promise { - const results = await original(); - - addCustomInspect(results); - - return results; - }; -} - function bsonMethod< K extends keyof PickMethodsByReturnType> >( @@ -214,36 +139,3 @@ function forwardedMethod< return (this._sp[key] as any)(...args); }; } - -function customDocumentInspect( - this: Document, - depth: number, - inspectOptions: InspectOptions -) { - const newInspectOptions = { - ...inspectOptions, - depth: Infinity, - maxArrayLength: Infinity, - maxStringLength: Infinity, - }; - - // reuse the standard inpect logic for an object without causing infinite - // recursion - const copyToInspect: any = Array.isArray(this) ? this.slice() : { ...this }; - delete copyToInspect[customInspectSymbol]; - return inspect(copyToInspect, newInspectOptions); -} - -function addCustomInspect(obj: any) { - if (Array.isArray(obj)) { - (obj as any)[customInspectSymbol] ??= customDocumentInspect; - for (const item of obj) { - addCustomInspect(item); - } - } else if (obj && typeof obj === 'object' && obj !== null && !obj._bsontype) { - obj[customInspectSymbol] = customDocumentInspect; - for (const value of Object.values(obj)) { - addCustomInspect(value); - } - } -} diff --git a/packages/shell-api/src/pick-methods-by-return-type.ts b/packages/shell-api/src/pick-methods-by-return-type.ts new file mode 100644 index 0000000000..20e8cf2050 --- /dev/null +++ b/packages/shell-api/src/pick-methods-by-return-type.ts @@ -0,0 +1,5 @@ +export type PickMethodsByReturnType = { + [k in keyof T as NonNullable extends (...args: any[]) => R + ? k + : never]: T[k]; +}; From 70aa3eefe5d70f6ca9d0d74cacb99671ac49ae7c Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 24 Nov 2025 13:28:45 +0000 Subject: [PATCH 11/33] don't accidentally override our custom Date and RegExp inpect functions --- packages/shell-api/src/custom-inspect.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/shell-api/src/custom-inspect.ts b/packages/shell-api/src/custom-inspect.ts index 7691c8e7fd..c2da9f9ab3 100644 --- a/packages/shell-api/src/custom-inspect.ts +++ b/packages/shell-api/src/custom-inspect.ts @@ -28,7 +28,14 @@ export function addCustomInspect(obj: any) { for (const item of obj) { addCustomInspect(item); } - } else if (obj && typeof obj === 'object' && obj !== null && !obj._bsontype) { + } else if ( + obj && + typeof obj === 'object' && + obj !== null && + !obj._bsontype && + !(obj instanceof Date) && + !(obj instanceof RegExp) + ) { obj[customInspectSymbol] = customDocumentInspect; for (const value of Object.values(obj)) { addCustomInspect(value); From 8624b68b18bdf12fa8ffa1c9e2e4c90ca0b8982f Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 24 Nov 2025 13:48:17 +0000 Subject: [PATCH 12/33] don't depend on inspect --- packages/shell-api/src/custom-inspect.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shell-api/src/custom-inspect.ts b/packages/shell-api/src/custom-inspect.ts index c2da9f9ab3..74925bdfb7 100644 --- a/packages/shell-api/src/custom-inspect.ts +++ b/packages/shell-api/src/custom-inspect.ts @@ -1,12 +1,12 @@ -import { inspect } from 'util'; -import type { InspectOptions } from 'util'; +import type { InspectOptions, inspect as _inspect } from 'util'; const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); function customDocumentInspect( this: Document, depth: number, - inspectOptions: InspectOptions + inspectOptions: InspectOptions, + inspect: typeof _inspect ) { const newInspectOptions = { ...inspectOptions, From cae4017b678ed493cfb8122a41156d229b717a02 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 24 Nov 2025 13:51:20 +0000 Subject: [PATCH 13/33] more indirection --- packages/e2e-tests/test/e2e-oidc.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-tests/test/e2e-oidc.spec.ts b/packages/e2e-tests/test/e2e-oidc.spec.ts index 08935926ca..50964a0fba 100644 --- a/packages/e2e-tests/test/e2e-oidc.spec.ts +++ b/packages/e2e-tests/test/e2e-oidc.spec.ts @@ -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._sp.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. From 7d2d0a308a486d22e6ed33b818593e1a32eb1a12 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 24 Nov 2025 16:46:29 +0000 Subject: [PATCH 14/33] fill out more things on the stub --- packages/shell-api/src/deep-inspect-service-provider-wrapper.ts | 2 +- packages/shell-api/src/shell-api.spec.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index bfda4fd83b..46d3d7e782 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -100,7 +100,7 @@ export class DeepInspectServiceProviderWrapper ...args: Parameters ): Promise { const sp = await this._sp.getNewConnection(...args); - return new DeepInspectServiceProviderWrapper(sp as ServiceProvider); + return new DeepInspectServiceProviderWrapper(sp); } } diff --git a/packages/shell-api/src/shell-api.spec.ts b/packages/shell-api/src/shell-api.spec.ts index ac93c68446..702b68c113 100644 --- a/packages/shell-api/src/shell-api.spec.ts +++ b/packages/shell-api/src/shell-api.spec.ts @@ -543,6 +543,8 @@ describe('ShellApi', function () { bus = new EventEmitter(); const newSP = stubInterface(); newSP.initialDb = 'test'; + newSP.platform = 'CLI'; + newSP.bsonLibrary = bson; serviceProvider = stubInterface({ getNewConnection: newSP, }); From d12548cb005948c54957ca828c30af9209da0b97 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Mon, 24 Nov 2025 16:57:27 +0000 Subject: [PATCH 15/33] how did this work before? --- packages/shell-api/src/runtime-independence.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/shell-api/src/runtime-independence.spec.ts b/packages/shell-api/src/runtime-independence.spec.ts index a19500e95e..a150a987be 100644 --- a/packages/shell-api/src/runtime-independence.spec.ts +++ b/packages/shell-api/src/runtime-independence.spec.ts @@ -59,11 +59,13 @@ describe('Runtime independence', function () { platform: 'CLI', close: sinon.spy(), bsonLibrary: absolutePathRequire(require.resolve('bson')).exports, + getURI: sinon.stub().returns('mongodb://localhost:27017'), + getFleOptions: sinon.stub().returns(undefined), }; const evaluationListener = { onExit: sinon.spy() }; const instanceState = new shellApi.ShellInstanceState(sp as any); instanceState.setEvaluationListener(evaluationListener); - expect(instanceState.initialServiceProvider).to.equal(sp); + expect((instanceState.initialServiceProvider as any)._sp).to.equal(sp); const bsonObj = instanceState.shellBson.ISODate( '2025-01-09T20:43:51+01:00' ); From 22e67f86506e91db34aa3b02779a3b567f804587 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Tue, 25 Nov 2025 10:48:38 +0000 Subject: [PATCH 16/33] add the custom inspect symbol as not-enumerable --- packages/shell-api/src/custom-inspect.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/shell-api/src/custom-inspect.ts b/packages/shell-api/src/custom-inspect.ts index 74925bdfb7..c2ced94f63 100644 --- a/packages/shell-api/src/custom-inspect.ts +++ b/packages/shell-api/src/custom-inspect.ts @@ -22,9 +22,20 @@ function customDocumentInspect( return inspect(copyToInspect, newInspectOptions); } +function addInspectSymbol(obj: any) { + if (!(obj as any)[customInspectSymbol]) { + Object.defineProperty(obj, customInspectSymbol, { + value: customDocumentInspect, + enumerable: false, + writable: true, + configurable: true, + }); + } +} + export function addCustomInspect(obj: any) { if (Array.isArray(obj)) { - (obj as any)[customInspectSymbol] ??= customDocumentInspect; + addInspectSymbol(obj); for (const item of obj) { addCustomInspect(item); } @@ -36,7 +47,7 @@ export function addCustomInspect(obj: any) { !(obj instanceof Date) && !(obj instanceof RegExp) ) { - obj[customInspectSymbol] = customDocumentInspect; + addInspectSymbol(obj); for (const value of Object.values(obj)) { addCustomInspect(value); } From f4f64bc1c3f2d006347ba1894fd2c4ab470d37c1 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Tue, 25 Nov 2025 12:10:47 +0000 Subject: [PATCH 17/33] pull bsonLibrary off _sp rather --- .../src/deep-inspect-service-provider-wrapper.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index 46d3d7e782..efc68edb90 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -1,5 +1,4 @@ import type { ServiceProvider } from '@mongosh/service-provider-core'; -import { ServiceProviderCore } from '@mongosh/service-provider-core'; import { DeepInspectAggregationCursorWrapper } from './deep-inspect-aggregation-cursor-wrapper'; import { DeepInspectFindCursorWrapper } from './deep-inspect-find-cursor-wrapper'; import { addCustomInspect } from './custom-inspect'; @@ -7,16 +6,15 @@ import type { PickMethodsByReturnType } from './pick-methods-by-return-type'; import { DeepInspectRunCommandCursorWrapper } from './deep-inspect-run-command-cursor-wrapper'; import { DeepInspectChangeStreamWrapper } from './deep-inspect-change-stream-wrapper'; -export class DeepInspectServiceProviderWrapper - extends ServiceProviderCore - implements ServiceProvider -{ +export class DeepInspectServiceProviderWrapper implements ServiceProvider { _sp: ServiceProvider; constructor(sp: ServiceProvider) { - super(sp.bsonLibrary); this._sp = sp; } + get bsonLibrary() { + return this._sp.bsonLibrary; + } aggregate = (...args: Parameters) => { const cursor = this._sp.aggregate(...args); From 77f64f211521f8306a70c9df2f9d2fdfeb70facf Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Thu, 27 Nov 2025 10:53:41 +0000 Subject: [PATCH 18/33] don't wrap the service provider in java land --- .../com/mongodb/mongosh/service/JavaServiceProvider.kt | 4 ++++ packages/service-provider-core/src/service-provider.ts | 5 ++++- .../shell-api/src/deep-inspect-service-provider-wrapper.ts | 2 ++ packages/shell-api/src/shell-instance-state.ts | 6 +++--- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/java-shell/src/main/kotlin/com/mongodb/mongosh/service/JavaServiceProvider.kt b/packages/java-shell/src/main/kotlin/com/mongodb/mongosh/service/JavaServiceProvider.kt index 824fb633dd..e24571a103 100644 --- a/packages/java-shell/src/main/kotlin/com/mongodb/mongosh/service/JavaServiceProvider.kt +++ b/packages/java-shell/src/main/kotlin/com/mongodb/mongosh/service/JavaServiceProvider.kt @@ -22,6 +22,10 @@ internal class JavaServiceProvider(private var client: MongoClient?, @HostAccess.Export val platform = "JavaShell" + @JvmField + @HostAccess.Export + val deepInspectWrappable = false + @HostAccess.Export override fun runCommand(database: String, spec: Value): Value = promise { getDatabase(database, null).map { db -> diff --git a/packages/service-provider-core/src/service-provider.ts b/packages/service-provider-core/src/service-provider.ts index a2bf63d569..fd949e26df 100644 --- a/packages/service-provider-core/src/service-provider.ts +++ b/packages/service-provider-core/src/service-provider.ts @@ -13,10 +13,13 @@ export default interface ServiceProvider extends Readable, Writable, Closable, - Admin {} + Admin { + deepInspectWrappable: boolean; +} export class ServiceProviderCore { public bsonLibrary: BSON; + public deepInspectWrappable = true; constructor(bsonLibrary?: BSON) { if (bsonLibrary === undefined) { throw new MongoshInternalError('BSON Library is undefined.'); diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index efc68edb90..b01d236cb5 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -9,6 +9,8 @@ import { DeepInspectChangeStreamWrapper } from './deep-inspect-change-stream-wra export class DeepInspectServiceProviderWrapper implements ServiceProvider { _sp: ServiceProvider; + deepInspectWrappable = false; + constructor(sp: ServiceProvider) { this._sp = sp; } diff --git a/packages/shell-api/src/shell-instance-state.ts b/packages/shell-api/src/shell-instance-state.ts index f948c94a7f..b867844a00 100644 --- a/packages/shell-api/src/shell-instance-state.ts +++ b/packages/shell-api/src/shell-instance-state.ts @@ -204,9 +204,9 @@ export class ShellInstanceState { cliOptions: ShellCliOptions = {}, bsonLibrary: BSONLibrary = initialServiceProvider.bsonLibrary ) { - this.initialServiceProvider = new DeepInspectServiceProviderWrapper( - initialServiceProvider - ); + this.initialServiceProvider = initialServiceProvider.deepInspectWrappable + ? new DeepInspectServiceProviderWrapper(initialServiceProvider) + : initialServiceProvider; this.bsonLibrary = bsonLibrary; this.messageBus = messageBus; this.shellApi = new ShellApi(this); From 72d8679556fd0237f0da3b9a69eee102cb1f6037 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Thu, 27 Nov 2025 11:02:36 +0000 Subject: [PATCH 19/33] Update packages/shell-api/src/custom-inspect.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/shell-api/src/custom-inspect.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shell-api/src/custom-inspect.ts b/packages/shell-api/src/custom-inspect.ts index c2ced94f63..e4675e9d0a 100644 --- a/packages/shell-api/src/custom-inspect.ts +++ b/packages/shell-api/src/custom-inspect.ts @@ -15,7 +15,7 @@ function customDocumentInspect( maxStringLength: Infinity, }; - // reuse the standard inpect logic for an object without causing infinite + // reuse the standard inspect logic for an object without causing infinite // recursion const copyToInspect: any = Array.isArray(this) ? this.slice() : { ...this }; delete copyToInspect[customInspectSymbol]; From a93b8fd891ed6e045b7e2bb988f42462309bfa6e Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Thu, 27 Nov 2025 13:06:27 +0000 Subject: [PATCH 20/33] some unit tests --- ...p-inspect-service-provider-wrapper.spec.ts | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts new file mode 100644 index 0000000000..16848b95dd --- /dev/null +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts @@ -0,0 +1,267 @@ +import { type ServiceProvider } from '@mongosh/service-provider-core'; +import * as bson from 'bson'; +import * as chai from 'chai'; +import { expect } from 'chai'; +import sinonChai from 'sinon-chai'; +import sinon from 'sinon'; +import type { StubbedInstance } from 'ts-sinon'; +import { stubInterface } from 'ts-sinon'; +import { DeepInspectServiceProviderWrapper } from './deep-inspect-service-provider-wrapper'; +import * as util from 'util'; +chai.use(sinonChai); + +const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); + +function truncatedString(text: string): boolean { + return /\d+ more character/.test(text); +} + +function truncatedArray(text: string): boolean { + return /\d+ more item/.test(text); +} +function truncatedObject(text: string): boolean { + return /\[Object\]/.test(text); +} + +function wasTruncated(text: string): boolean { + return truncatedString(text) || truncatedArray(text) || truncatedObject(text); +} + +describe('DeepInspectServiceProviderWrapper', function () { + let serviceProvider: StubbedInstance; + let sp: DeepInspectServiceProviderWrapper; + + const doc = { + array: Array.from(Array(1000), (_, i) => i), + string: 'All work and no play makes Jack a dull boy. '.repeat(250), + object: { + foo: { + bar: { + baz: { + qux: { + quux: { + corge: { + grault: 'If you can read this, you are too close.', + }, + }, + }, + }, + }, + }, + }, + }; + + function checkResultDoc(result: any) { + expect(result).to.deep.equal(doc); + expect((result as any)[customInspectSymbol]).to.be.a('function'); + expect((result as any).array[customInspectSymbol]).to.be.a('function'); + expect((result as any).object[customInspectSymbol]).to.be.a('function'); + expect((result as any).object.foo[customInspectSymbol]).to.be.a('function'); + expect(wasTruncated(util.inspect(result))).to.equal(false); + expect(wasTruncated(util.inspect(result?.array))).to.equal(false); + expect(wasTruncated(util.inspect(result?.object))).to.equal(false); + expect(wasTruncated(util.inspect(result?.object.foo))).to.equal(false); + } + + const everyType = { + double: new bson.Double(1.2), + doubleThatIsAlsoAnInteger: new bson.Double(1), + string: 'Hello, world!', + binData: new bson.Binary(Buffer.from([1, 2, 3])), + boolean: true, + date: new Date('2023-04-05T13:25:08.445Z'), + null: null, + regex: new bson.BSONRegExp('pattern', 'i'), + javascript: new bson.Code('function() {}'), + symbol: new bson.BSONSymbol('symbol'), + javascriptWithScope: new bson.Code('function() {}', { foo: 1, bar: 'a' }), + int: new bson.Int32(12345), + timestamp: new bson.Timestamp(bson.Long.fromString('7218556297505931265')), + long: bson.Long.fromString('123456789123456789'), + decimal: new bson.Decimal128( + Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) + ), + minKey: new bson.MinKey(), + maxKey: new bson.MaxKey(), + + binaries: { + generic: new bson.Binary(Buffer.from([1, 2, 3]), 0), + functionData: new bson.Binary(Buffer.from('//8='), 1), + binaryOld: new bson.Binary(Buffer.from('//8='), 2), + uuidOld: new bson.Binary(Buffer.from('c//SZESzTGmQ6OfR38A11A=='), 3), + uuid: new bson.UUID('AAAAAAAA-AAAA-4AAA-AAAA-AAAAAAAAAAAA'), + md5: new bson.Binary(Buffer.from('c//SZESzTGmQ6OfR38A11A=='), 5), + encrypted: new bson.Binary(Buffer.from('c//SZESzTGmQ6OfR38A11A=='), 6), + compressedTimeSeries: new bson.Binary( + Buffer.from( + 'CQCKW/8XjAEAAIfx//////////H/////////AQAAAAAAAABfAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAAHAAAAAAAAAA4AAAAAAAAAAA==', + 'base64' + ) + ), + custom: new bson.Binary(Buffer.from('//8='), 128), + }, + + dbRef: new bson.DBRef( + 'namespace', + new bson.ObjectId('642d76b4b7ebfab15d3c4a78') + ), + }; + + beforeEach(function () { + serviceProvider = stubInterface(); + serviceProvider.initialDb = 'db1'; + serviceProvider.bsonLibrary = bson; + sp = new DeepInspectServiceProviderWrapper(serviceProvider); + }); + + it('would have truncated the test documents without deep inspection', function () { + // make sure that our assertions would have caught truncation if it were to happen + const text = util.inspect(doc); + expect(truncatedString(text)).to.equal(true); + expect(truncatedArray(text)).to.equal(true); + expect(truncatedObject(text)).to.equal(true); + expect(wasTruncated(text)).to.equal(true); + }); + + it('wraps forwarded methods', async function () { + serviceProvider.count.resolves(5); + const result = await sp.count('testDb', 'testColl', {}); + expect(result).to.equal(5); + }); + + it('wraps bson methods', async function () { + serviceProvider.runCommand.resolves(doc); + const result = await sp.runCommand('testDb', {}, {}, {}); + checkResultDoc(result); + }); + + it('wraps find cursors', async function () { + const stubs = { + // forwarded method + allowDiskUse: sinon.stub(), + + // forwarded method that returns this for chaining + withReadPreference: sinon.stub().returnsThis(), + + // methods that return results, promises of results, etc. + next: sinon.stub().resolves(doc), + tryNext: sinon.stub().resolves(doc), + toArray: sinon.stub().resolves([doc, everyType]), + readBufferedDocuments: sinon.stub().returns([doc, everyType]), + }; + serviceProvider.find.returns(stubs as any); + + const cursor = sp.find('testDb', 'testColl', {}, {}, {}); + + cursor.withReadPreference('primary').allowDiskUse(); + expect(stubs.withReadPreference).to.have.been.calledOnce; + expect(stubs.allowDiskUse).to.have.been.calledOnce; + + const nextResult = await cursor.next(); + checkResultDoc(nextResult); + + const tryNextResult = await cursor.tryNext(); + checkResultDoc(tryNextResult); + + const toArrayResult = await cursor.toArray(); + expect(toArrayResult).to.deep.equal([doc, everyType]); + checkResultDoc(toArrayResult[0]); + + const readBufferedDocumentsResult = cursor.readBufferedDocuments(); + expect(readBufferedDocumentsResult).to.deep.equal([doc, everyType]); + checkResultDoc(readBufferedDocumentsResult[0]); + }); + + it('wraps aggregation cursors', async function () { + const stubs = { + // forwarded method + project: sinon.stub(), + + // forwarded method that returns this for chaining + withReadPreference: sinon.stub().returnsThis(), + + // methods that return results, promises of results, etc. + next: sinon.stub().resolves(doc), + tryNext: sinon.stub().resolves(doc), + toArray: sinon.stub().resolves([doc, everyType]), + readBufferedDocuments: sinon.stub().returns([doc, everyType]), + }; + serviceProvider.aggregate.returns(stubs as any); + + const cursor = sp.aggregate('testDb', 'testColl', [], {}, {}); + + cursor.withReadPreference('primary').project({}); + expect(stubs.withReadPreference).to.have.been.calledOnce; + expect(stubs.project).to.have.been.calledOnce; + + const nextResult = await cursor.next(); + checkResultDoc(nextResult); + + const tryNextResult = await cursor.tryNext(); + checkResultDoc(tryNextResult); + + const toArrayResult = await cursor.toArray(); + checkResultDoc(toArrayResult[0]); + + const readBufferedDocumentsResult = cursor.readBufferedDocuments(); + checkResultDoc(readBufferedDocumentsResult[0]); + }); + + it('wraps run command cursors', async function () { + const stubs = { + // forwarded method + batchSize: sinon.stub(), + + // methods that return results, promises of results, etc. + next: sinon.stub().resolves(doc), + tryNext: sinon.stub().resolves(doc), + toArray: sinon.stub().resolves([doc, everyType]), + readBufferedDocuments: sinon.stub().returns([doc, everyType]), + }; + serviceProvider.runCursorCommand.returns(stubs as any); + + const cursor = sp.runCursorCommand('testDb', {}, {}, {}); + + cursor.batchSize(10); + expect(stubs.batchSize).to.have.been.calledOnce; + + const nextResult = await cursor.next(); + checkResultDoc(nextResult); + + const tryNextResult = await cursor.tryNext(); + checkResultDoc(tryNextResult); + + const toArrayResult = await cursor.toArray(); + expect(toArrayResult).to.deep.equal([doc, everyType]); + checkResultDoc(toArrayResult[0]); + + const readBufferedDocumentsResult = cursor.readBufferedDocuments(); + expect(readBufferedDocumentsResult).to.deep.equal([doc, everyType]); + checkResultDoc(readBufferedDocumentsResult[0]); + }); + + it('wraps change streams', async function () { + const stubs = { + // forwarded method + hasNext: sinon.stub().resolves(true), + + // methods that return results, promises of results, etc. + next: sinon.stub().resolves(doc), + tryNext: sinon.stub().resolves(doc), + toArray: sinon.stub().resolves([doc, everyType]), + readBufferedDocuments: sinon.stub().returns([doc, everyType]), + }; + serviceProvider.watch.returns(stubs as any); + + const cursor = sp.watch([], {}, {}); + + await cursor.hasNext(); + expect(stubs.hasNext).to.have.been.calledOnce; + + const nextResult = await cursor.next(); + checkResultDoc(nextResult); + + const tryNextResult = await cursor.tryNext(); + checkResultDoc(tryNextResult); + }); +}); From 0ce95c38d41f297db52ea7935f4245a88d941eaa Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Thu, 27 Nov 2025 14:16:33 +0000 Subject: [PATCH 21/33] more unit tests --- ...p-inspect-service-provider-wrapper.spec.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts index 16848b95dd..5159845892 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts @@ -8,6 +8,8 @@ import type { StubbedInstance } from 'ts-sinon'; import { stubInterface } from 'ts-sinon'; import { DeepInspectServiceProviderWrapper } from './deep-inspect-service-provider-wrapper'; import * as util from 'util'; +import { makePrintableBson } from '@mongosh/shell-bson'; + chai.use(sinonChai); const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); @@ -31,6 +33,10 @@ describe('DeepInspectServiceProviderWrapper', function () { let serviceProvider: StubbedInstance; let sp: DeepInspectServiceProviderWrapper; + // make the tests behave the same regardless of whether this file was focused + // or not + makePrintableBson(bson); + const doc = { array: Array.from(Array(1000), (_, i) => i), string: 'All work and no play makes Jack a dull boy. '.repeat(250), @@ -107,6 +113,45 @@ describe('DeepInspectServiceProviderWrapper', function () { ), }; + function checkResultEveryType(result: any) { + expect(result).to.deep.equal(everyType); + expect(wasTruncated(util.inspect(result))).to.equal(false); + + // this makes sure that we didn't accidentally mess with the custom inspect + // methods of the BSON types, dates, regexes or simple values + expect(util.inspect(result)).to.equal(`{ + double: Double(1.2), + doubleThatIsAlsoAnInteger: Double(1), + string: 'Hello, world!', + binData: Binary.createFromBase64('AQID', 0), + boolean: true, + date: 2023-04-05T13:25:08.445Z, + null: null, + regex: BSONRegExp('pattern', 'i'), + javascript: Code('function() {}'), + symbol: BSONSymbol('symbol'), + javascriptWithScope: Code('function() {}', { foo: 1, bar: 'a' }), + int: Int32(12345), + timestamp: Timestamp({ t: 1680701109, i: 1 }), + long: Long('123456789123456789'), + decimal: Decimal128('5.477284286264328586719275128128001E-4088'), + minKey: MinKey(), + maxKey: MaxKey(), + binaries: { + generic: Binary.createFromBase64('AQID', 0), + functionData: Binary.createFromBase64('Ly84PQ==', 1), + binaryOld: Binary.createFromBase64('Ly84PQ==', 2), + uuidOld: Binary.createFromBase64('Yy8vU1pFU3pUR21RNk9mUjM4QTExQT09', 3), + uuid: UUID('aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa'), + md5: MD5('632f2f535a45537a54476d51364f66523338413131413d3d'), + encrypted: Binary.createFromBase64('Yy8vU1pFU3pUR21RNk9mUjM4QTExQT09', 6), + compressedTimeSeries: Binary.createFromBase64('CQCKW/8XjAEAAIfx//////////H/////////AQAAAAAAAABfAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAAHAAAAAAAAAA4AAAAAAAAAAA==', 0), + custom: Binary.createFromBase64('Ly84PQ==', 128) + }, + dbRef: DBRef('namespace', ObjectId('642d76b4b7ebfab15d3c4a78')) +}`); + } + beforeEach(function () { serviceProvider = stubInterface(); serviceProvider.initialDb = 'db1'; @@ -166,10 +211,12 @@ describe('DeepInspectServiceProviderWrapper', function () { const toArrayResult = await cursor.toArray(); expect(toArrayResult).to.deep.equal([doc, everyType]); checkResultDoc(toArrayResult[0]); + checkResultEveryType(toArrayResult[1]); const readBufferedDocumentsResult = cursor.readBufferedDocuments(); expect(readBufferedDocumentsResult).to.deep.equal([doc, everyType]); checkResultDoc(readBufferedDocumentsResult[0]); + checkResultEveryType(readBufferedDocumentsResult[1]); }); it('wraps aggregation cursors', async function () { @@ -202,9 +249,11 @@ describe('DeepInspectServiceProviderWrapper', function () { const toArrayResult = await cursor.toArray(); checkResultDoc(toArrayResult[0]); + checkResultEveryType(toArrayResult[1]); const readBufferedDocumentsResult = cursor.readBufferedDocuments(); checkResultDoc(readBufferedDocumentsResult[0]); + checkResultEveryType(readBufferedDocumentsResult[1]); }); it('wraps run command cursors', async function () { @@ -234,10 +283,12 @@ describe('DeepInspectServiceProviderWrapper', function () { const toArrayResult = await cursor.toArray(); expect(toArrayResult).to.deep.equal([doc, everyType]); checkResultDoc(toArrayResult[0]); + checkResultEveryType(toArrayResult[1]); const readBufferedDocumentsResult = cursor.readBufferedDocuments(); expect(readBufferedDocumentsResult).to.deep.equal([doc, everyType]); checkResultDoc(readBufferedDocumentsResult[0]); + checkResultEveryType(readBufferedDocumentsResult[1]); }); it('wraps change streams', async function () { From 69d3d13bfc0defc5ca9f7ef708563f7d486f0fda Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Thu, 27 Nov 2025 14:27:36 +0000 Subject: [PATCH 22/33] adjust runtime indepdendence tests --- packages/shell-api/src/runtime-independence.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shell-api/src/runtime-independence.spec.ts b/packages/shell-api/src/runtime-independence.spec.ts index a150a987be..b695e38709 100644 --- a/packages/shell-api/src/runtime-independence.spec.ts +++ b/packages/shell-api/src/runtime-independence.spec.ts @@ -56,6 +56,7 @@ describe('Runtime independence', function () { // Verify that `shellApi` is generally usable. const sp = { + deepInspectWrappable: true, platform: 'CLI', close: sinon.spy(), bsonLibrary: absolutePathRequire(require.resolve('bson')).exports, From 209c9f594ee4d2115608f981399c99300c05577c Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Thu, 27 Nov 2025 14:28:37 +0000 Subject: [PATCH 23/33] Update packages/shell-api/src/custom-inspect.ts Co-authored-by: Anna Henningsen --- packages/shell-api/src/custom-inspect.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/shell-api/src/custom-inspect.ts b/packages/shell-api/src/custom-inspect.ts index e4675e9d0a..e74791499b 100644 --- a/packages/shell-api/src/custom-inspect.ts +++ b/packages/shell-api/src/custom-inspect.ts @@ -44,8 +44,9 @@ export function addCustomInspect(obj: any) { typeof obj === 'object' && obj !== null && !obj._bsontype && - !(obj instanceof Date) && - !(obj instanceof RegExp) + !['[object Date]', '[object RegExp]'].includes( + Object.prototype.toString.call(obj) + ) ) { addInspectSymbol(obj); for (const value of Object.values(obj)) { From c023d4693b89e913ff1927481ef5af148c127b17 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 27 Nov 2025 16:43:21 +0100 Subject: [PATCH 24/33] fixup: remove wrappable flag, skip async iter support for now, use fn instead of class --- .../mongosh/service/JavaServiceProvider.kt | 4 - .../collection/insertOne.expected.1.txt | 12 + packages/service-provider-core/src/cursors.ts | 2 +- .../src/service-provider.ts | 5 +- packages/shell-api/src/abstract-cursor.ts | 8 +- packages/shell-api/src/custom-inspect.ts | 16 +- ...deep-inspect-aggregation-cursor-wrapper.ts | 4 +- .../src/deep-inspect-change-stream-wrapper.ts | 4 +- .../src/deep-inspect-find-cursor-wrapper.ts | 4 +- ...deep-inspect-run-command-cursor-wrapper.ts | 4 +- ...p-inspect-service-provider-wrapper.spec.ts | 8 +- .../deep-inspect-service-provider-wrapper.ts | 207 ++++++++---------- .../src/runtime-independence.spec.ts | 1 - .../shell-api/src/shell-instance-state.ts | 8 +- 14 files changed, 139 insertions(+), 148 deletions(-) create mode 100644 packages/java-shell/src/test/resources/collection/insertOne.expected.1.txt diff --git a/packages/java-shell/src/main/kotlin/com/mongodb/mongosh/service/JavaServiceProvider.kt b/packages/java-shell/src/main/kotlin/com/mongodb/mongosh/service/JavaServiceProvider.kt index e24571a103..824fb633dd 100644 --- a/packages/java-shell/src/main/kotlin/com/mongodb/mongosh/service/JavaServiceProvider.kt +++ b/packages/java-shell/src/main/kotlin/com/mongodb/mongosh/service/JavaServiceProvider.kt @@ -22,10 +22,6 @@ internal class JavaServiceProvider(private var client: MongoClient?, @HostAccess.Export val platform = "JavaShell" - @JvmField - @HostAccess.Export - val deepInspectWrappable = false - @HostAccess.Export override fun runCommand(database: String, spec: Value): Value = promise { getDatabase(database, null).map { db -> diff --git a/packages/java-shell/src/test/resources/collection/insertOne.expected.1.txt b/packages/java-shell/src/test/resources/collection/insertOne.expected.1.txt new file mode 100644 index 0000000000..80717e69d3 --- /dev/null +++ b/packages/java-shell/src/test/resources/collection/insertOne.expected.1.txt @@ -0,0 +1,12 @@ +{ "acknowledged": true, "insertedId": } +true +[ { "_id": , "a": 1, "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": } ] +{ "acknowledged": true, "insertedId": null } +{ "acknowledged": true, "insertedId": {"$date": {"$numberLong": "1355875200000"}} } +{ "acknowledged": true, "insertedId": } +{ "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" } } \ No newline at end of file diff --git a/packages/service-provider-core/src/cursors.ts b/packages/service-provider-core/src/cursors.ts index 126e71eefd..ee86666fef 100644 --- a/packages/service-provider-core/src/cursors.ts +++ b/packages/service-provider-core/src/cursors.ts @@ -15,7 +15,7 @@ export interface ServiceProviderBaseCursor { next(): Promise; tryNext(): Promise; readonly closed: boolean; - [Symbol.asyncIterator](): AsyncGenerator; + [Symbol.asyncIterator]?(): AsyncGenerator; } export interface ServiceProviderAbstractCursor diff --git a/packages/service-provider-core/src/service-provider.ts b/packages/service-provider-core/src/service-provider.ts index fd949e26df..a2bf63d569 100644 --- a/packages/service-provider-core/src/service-provider.ts +++ b/packages/service-provider-core/src/service-provider.ts @@ -13,13 +13,10 @@ export default interface ServiceProvider extends Readable, Writable, Closable, - Admin { - deepInspectWrappable: boolean; -} + Admin {} export class ServiceProviderCore { public bsonLibrary: BSON; - public deepInspectWrappable = true; constructor(bsonLibrary?: BSON) { if (bsonLibrary === undefined) { throw new MongoshInternalError('BSON Library is undefined.'); diff --git a/packages/shell-api/src/abstract-cursor.ts b/packages/shell-api/src/abstract-cursor.ts index 83b58d71a4..11821896e2 100644 --- a/packages/shell-api/src/abstract-cursor.ts +++ b/packages/shell-api/src/abstract-cursor.ts @@ -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; } diff --git a/packages/shell-api/src/custom-inspect.ts b/packages/shell-api/src/custom-inspect.ts index e74791499b..4282e946d0 100644 --- a/packages/shell-api/src/custom-inspect.ts +++ b/packages/shell-api/src/custom-inspect.ts @@ -24,12 +24,16 @@ function customDocumentInspect( function addInspectSymbol(obj: any) { if (!(obj as any)[customInspectSymbol]) { - Object.defineProperty(obj, customInspectSymbol, { - value: customDocumentInspect, - enumerable: false, - writable: true, - configurable: true, - }); + try { + Object.defineProperty(obj, customInspectSymbol, { + value: customDocumentInspect, + enumerable: false, + writable: true, + configurable: true, + }); + } catch { + // Ignore, if the object is non-extensible we cannot do much about that + } } } diff --git a/packages/shell-api/src/deep-inspect-aggregation-cursor-wrapper.ts b/packages/shell-api/src/deep-inspect-aggregation-cursor-wrapper.ts index 6812dd9084..5e6dd67759 100644 --- a/packages/shell-api/src/deep-inspect-aggregation-cursor-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-aggregation-cursor-wrapper.ts @@ -47,10 +47,10 @@ export class DeepInspectAggregationCursorWrapper return this._cursor.closed; } - async *[Symbol.asyncIterator]() { + /*async *[Symbol.asyncIterator]() { yield* this._cursor; return; - } + }*/ } function forwardResultPromise< diff --git a/packages/shell-api/src/deep-inspect-change-stream-wrapper.ts b/packages/shell-api/src/deep-inspect-change-stream-wrapper.ts index 21b547488b..c788ee69f8 100644 --- a/packages/shell-api/src/deep-inspect-change-stream-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-change-stream-wrapper.ts @@ -29,10 +29,10 @@ export class DeepInspectChangeStreamWrapper return this._cursor.closed; } - async *[Symbol.asyncIterator]() { + /*async *[Symbol.asyncIterator]() { yield* this._cursor; return; - } + }*/ } function forwardResultPromise< diff --git a/packages/shell-api/src/deep-inspect-find-cursor-wrapper.ts b/packages/shell-api/src/deep-inspect-find-cursor-wrapper.ts index f008d45879..a0aff655e5 100644 --- a/packages/shell-api/src/deep-inspect-find-cursor-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-find-cursor-wrapper.ts @@ -61,10 +61,10 @@ export class DeepInspectFindCursorWrapper return this._cursor.closed; } - async *[Symbol.asyncIterator]() { + /*async *[Symbol.asyncIterator]() { yield* this._cursor; return; - } + }*/ } function forwardResultPromise< diff --git a/packages/shell-api/src/deep-inspect-run-command-cursor-wrapper.ts b/packages/shell-api/src/deep-inspect-run-command-cursor-wrapper.ts index a918f4ae3a..83f86c717d 100644 --- a/packages/shell-api/src/deep-inspect-run-command-cursor-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-run-command-cursor-wrapper.ts @@ -32,10 +32,10 @@ export class DeepInspectRunCommandCursorWrapper return this._cursor.closed; } - async *[Symbol.asyncIterator]() { + /*async *[Symbol.asyncIterator]() { yield* this._cursor; return; - } + }*/ } function forwardResultPromise< diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts index 5159845892..5fbccdba13 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts @@ -6,7 +6,7 @@ import sinonChai from 'sinon-chai'; import sinon from 'sinon'; import type { StubbedInstance } from 'ts-sinon'; import { stubInterface } from 'ts-sinon'; -import { DeepInspectServiceProviderWrapper } from './deep-inspect-service-provider-wrapper'; +import { deepInspectServiceProviderWrapper } from './deep-inspect-service-provider-wrapper'; import * as util from 'util'; import { makePrintableBson } from '@mongosh/shell-bson'; @@ -29,9 +29,9 @@ function wasTruncated(text: string): boolean { return truncatedString(text) || truncatedArray(text) || truncatedObject(text); } -describe('DeepInspectServiceProviderWrapper', function () { +describe('deepInspectServiceProviderWrapper', function () { let serviceProvider: StubbedInstance; - let sp: DeepInspectServiceProviderWrapper; + let sp: ServiceProvider; // make the tests behave the same regardless of whether this file was focused // or not @@ -156,7 +156,7 @@ describe('DeepInspectServiceProviderWrapper', function () { serviceProvider = stubInterface(); serviceProvider.initialDb = 'db1'; serviceProvider.bsonLibrary = bson; - sp = new DeepInspectServiceProviderWrapper(serviceProvider); + sp = deepInspectServiceProviderWrapper(serviceProvider); }); it('would have truncated the test documents without deep inspection', function () { diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts index b01d236cb5..7f4afb1bcd 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts @@ -6,136 +6,121 @@ import type { PickMethodsByReturnType } from './pick-methods-by-return-type'; import { DeepInspectRunCommandCursorWrapper } from './deep-inspect-run-command-cursor-wrapper'; import { DeepInspectChangeStreamWrapper } from './deep-inspect-change-stream-wrapper'; -export class DeepInspectServiceProviderWrapper implements ServiceProvider { - _sp: ServiceProvider; - - deepInspectWrappable = false; - - constructor(sp: ServiceProvider) { - this._sp = sp; - } - get bsonLibrary() { - return this._sp.bsonLibrary; - } - - aggregate = (...args: Parameters) => { - const cursor = this._sp.aggregate(...args); - return new DeepInspectAggregationCursorWrapper(cursor); - }; - aggregateDb = (...args: Parameters) => { - const cursor = this._sp.aggregateDb(...args); - return new DeepInspectAggregationCursorWrapper(cursor); - }; - count = forwardedMethod('count'); - estimatedDocumentCount = forwardedMethod('estimatedDocumentCount'); - countDocuments = forwardedMethod('countDocuments'); - distinct = bsonMethod('distinct'); - find = (...args: Parameters) => { - const cursor = this._sp.find(...args); - return new DeepInspectFindCursorWrapper(cursor); - }; - findOneAndDelete = bsonMethod('findOneAndDelete'); - findOneAndReplace = bsonMethod('findOneAndReplace'); - findOneAndUpdate = bsonMethod('findOneAndUpdate'); - getTopologyDescription = forwardedMethod('getTopologyDescription'); - getIndexes = bsonMethod('getIndexes'); - listCollections = bsonMethod('listCollections'); - readPreferenceFromOptions = forwardedMethod('readPreferenceFromOptions'); - watch = (...args: Parameters) => { - const cursor = this._sp.watch(...args); - return new DeepInspectChangeStreamWrapper(cursor); +export function deepInspectServiceProviderWrapper( + sp: ServiceProvider +): ServiceProvider { + return { + get bsonLibrary() { + return sp.bsonLibrary; + }, + aggregate: (...args: Parameters) => { + const cursor = sp.aggregate(...args); + return new DeepInspectAggregationCursorWrapper(cursor); + }, + aggregateDb: (...args: Parameters) => { + const cursor = sp.aggregateDb(...args); + return new DeepInspectAggregationCursorWrapper(cursor); + }, + count: forwardedMethod('count', sp), + estimatedDocumentCount: forwardedMethod('estimatedDocumentCount', sp), + countDocuments: forwardedMethod('countDocuments', sp), + distinct: bsonMethod('distinct', sp), + find: (...args: Parameters) => { + const cursor = sp.find(...args); + return new DeepInspectFindCursorWrapper(cursor); + }, + findOneAndDelete: bsonMethod('findOneAndDelete', sp), + findOneAndReplace: bsonMethod('findOneAndReplace', sp), + findOneAndUpdate: bsonMethod('findOneAndUpdate', sp), + getTopologyDescription: forwardedMethod('getTopologyDescription', sp), + getIndexes: bsonMethod('getIndexes', sp), + listCollections: bsonMethod('listCollections', sp), + readPreferenceFromOptions: forwardedMethod('readPreferenceFromOptions', sp), + watch: (...args: Parameters) => { + const cursor = sp.watch(...args); + return new DeepInspectChangeStreamWrapper(cursor); + }, + getSearchIndexes: bsonMethod('getSearchIndexes', sp), + runCommand: bsonMethod('runCommand', sp), + runCommandWithCheck: bsonMethod('runCommandWithCheck', sp), + runCursorCommand: ( + ...args: Parameters + ) => { + const cursor = sp.runCursorCommand(...args); + return new DeepInspectRunCommandCursorWrapper(cursor); + }, + dropDatabase: bsonMethod('dropDatabase', sp), + dropCollection: forwardedMethod('dropCollection', sp), + bulkWrite: bsonMethod('bulkWrite', sp), + clientBulkWrite: bsonMethod('clientBulkWrite', sp), + deleteMany: bsonMethod('deleteMany', sp), + updateMany: bsonMethod('updateMany', sp), + updateOne: bsonMethod('updateOne', sp), + deleteOne: bsonMethod('deleteOne', sp), + createIndexes: bsonMethod('createIndexes', sp), + insertMany: bsonMethod('insertMany', sp), + insertOne: bsonMethod('insertOne', sp), + replaceOne: bsonMethod('replaceOne', sp), + initializeBulkOp: forwardedMethod('initializeBulkOp', sp), // you cannot extend the return value here + createSearchIndexes: forwardedMethod('createSearchIndexes', sp), + close: forwardedMethod('close', sp), + suspend: forwardedMethod('suspend', sp), + renameCollection: forwardedMethod('renameCollection', sp), + dropSearchIndex: forwardedMethod('dropSearchIndex', sp), + updateSearchIndex: forwardedMethod('updateSearchIndex', sp), + listDatabases: bsonMethod('listDatabases', sp), + authenticate: forwardedMethod('authenticate', sp), + createCollection: forwardedMethod('createCollection', sp), + getReadPreference: forwardedMethod('getReadPreference', sp), + getReadConcern: forwardedMethod('getReadConcern', sp), + getWriteConcern: forwardedMethod('getWriteConcern', sp), + get platform() { + return sp.platform; + }, + get initialDb() { + return sp.initialDb; + }, + getURI: forwardedMethod('getURI', sp), + getConnectionInfo: forwardedMethod('getConnectionInfo', sp), + resetConnectionOptions: forwardedMethod('resetConnectionOptions', sp), + startSession: forwardedMethod('startSession', sp), + getRawClient: forwardedMethod('getRawClient', sp), + createClientEncryption: forwardedMethod('createClientEncryption', sp), + getFleOptions: forwardedMethod('getFleOptions', sp), + createEncryptedCollection: forwardedMethod('createEncryptedCollection', sp), + async getNewConnection( + ...args: Parameters + ): Promise { + const newSp = await sp.getNewConnection(...args); + return deepInspectServiceProviderWrapper(newSp); + }, }; - getSearchIndexes = bsonMethod('getSearchIndexes'); - runCommand = bsonMethod('runCommand'); - runCommandWithCheck = bsonMethod('runCommandWithCheck'); - runCursorCommand = ( - ...args: Parameters - ) => { - const cursor = this._sp.runCursorCommand(...args); - return new DeepInspectRunCommandCursorWrapper(cursor); - }; - dropDatabase = bsonMethod('dropDatabase'); - dropCollection = forwardedMethod('dropCollection'); - bulkWrite = bsonMethod('bulkWrite'); - clientBulkWrite = bsonMethod('clientBulkWrite'); - deleteMany = bsonMethod('deleteMany'); - updateMany = bsonMethod('updateMany'); - updateOne = bsonMethod('updateOne'); - deleteOne = bsonMethod('deleteOne'); - createIndexes = bsonMethod('createIndexes'); - insertMany = bsonMethod('insertMany'); - insertOne = bsonMethod('insertOne'); - replaceOne = bsonMethod('replaceOne'); - initializeBulkOp = forwardedMethod('initializeBulkOp'); // you cannot extend the return value here - createSearchIndexes = forwardedMethod('createSearchIndexes'); - close = forwardedMethod('close'); - suspend = forwardedMethod('suspend'); - renameCollection = forwardedMethod('renameCollection'); - dropSearchIndex = forwardedMethod('dropSearchIndex'); - updateSearchIndex = forwardedMethod('updateSearchIndex'); - listDatabases = bsonMethod('listDatabases'); - authenticate = forwardedMethod('authenticate'); - createCollection = forwardedMethod('createCollection'); - getReadPreference = forwardedMethod('getReadPreference'); - getReadConcern = forwardedMethod('getReadConcern'); - getWriteConcern = forwardedMethod('getWriteConcern'); - - get platform() { - return this._sp.platform; - } - get initialDb() { - return this._sp.initialDb; - } - - getURI = forwardedMethod('getURI'); - getConnectionInfo = forwardedMethod('getConnectionInfo'); - resetConnectionOptions = forwardedMethod('resetConnectionOptions'); - startSession = forwardedMethod('startSession'); - getRawClient = forwardedMethod('getRawClient'); - createClientEncryption = forwardedMethod('createClientEncryption'); - getFleOptions = forwardedMethod('getFleOptions'); - createEncryptedCollection = forwardedMethod('createEncryptedCollection'); - - async getNewConnection( - ...args: Parameters - ): Promise { - const sp = await this._sp.getNewConnection(...args); - return new DeepInspectServiceProviderWrapper(sp); - } } function bsonMethod< K extends keyof PickMethodsByReturnType> ->( - key: K -): ( - ...args: Parameters[K]> -) => ReturnType[K]> { +>(key: K, sp: ServiceProvider): ServiceProvider[K] { + if (!sp[key]) return undefined as ServiceProvider[K]; return async function ( - this: DeepInspectServiceProviderWrapper, ...args: Parameters[K]> ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore The returntype already contains a promise ReturnType[K]> { - const result = await (this._sp[key] as any)(...args); + const result = await (sp[key] as any)(...args); addCustomInspect(result); return result; - }; + } as ServiceProvider[K]; } function forwardedMethod< K extends keyof PickMethodsByReturnType ->( - key: K -): ( - ...args: Parameters[K]> -) => ReturnType[K]> { +>(key: K, sp: ServiceProvider): ServiceProvider[K] { + if (!sp[key]) return undefined as ServiceProvider[K]; return function ( - this: DeepInspectServiceProviderWrapper, ...args: Parameters[K]> ): ReturnType[K]> { // not wrapping the result at all because forwardedMethod() is for simple // values only - return (this._sp[key] as any)(...args); - }; + return (sp[key] as any)(...args); + } as ServiceProvider[K]; } diff --git a/packages/shell-api/src/runtime-independence.spec.ts b/packages/shell-api/src/runtime-independence.spec.ts index b695e38709..a150a987be 100644 --- a/packages/shell-api/src/runtime-independence.spec.ts +++ b/packages/shell-api/src/runtime-independence.spec.ts @@ -56,7 +56,6 @@ describe('Runtime independence', function () { // Verify that `shellApi` is generally usable. const sp = { - deepInspectWrappable: true, platform: 'CLI', close: sinon.spy(), bsonLibrary: absolutePathRequire(require.resolve('bson')).exports, diff --git a/packages/shell-api/src/shell-instance-state.ts b/packages/shell-api/src/shell-instance-state.ts index b867844a00..651a5008d7 100644 --- a/packages/shell-api/src/shell-instance-state.ts +++ b/packages/shell-api/src/shell-instance-state.ts @@ -51,7 +51,7 @@ import type { AutocompletionContext } from '@mongodb-js/mongodb-ts-autocomplete' import type { JSONSchema } from 'mongodb-schema'; import { analyzeDocuments } from 'mongodb-schema'; import type { BaseCursor } from './abstract-cursor'; -import { DeepInspectServiceProviderWrapper } from './deep-inspect-service-provider-wrapper'; +import { deepInspectServiceProviderWrapper } from './deep-inspect-service-provider-wrapper'; /** * The subset of CLI options that is relevant for the shell API's behavior itself. @@ -204,9 +204,9 @@ export class ShellInstanceState { cliOptions: ShellCliOptions = {}, bsonLibrary: BSONLibrary = initialServiceProvider.bsonLibrary ) { - this.initialServiceProvider = initialServiceProvider.deepInspectWrappable - ? new DeepInspectServiceProviderWrapper(initialServiceProvider) - : initialServiceProvider; + this.initialServiceProvider = deepInspectServiceProviderWrapper( + initialServiceProvider + ); this.bsonLibrary = bsonLibrary; this.messageBus = messageBus; this.shellApi = new ShellApi(this); From e31ba3567b21d89f39c1353c283269d31c4f34c1 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 27 Nov 2025 17:55:17 +0100 Subject: [PATCH 25/33] fixup: merge cursor implementations, move to separate subdir --- ...deep-inspect-aggregation-cursor-wrapper.ts | 142 ---------------- .../src/deep-inspect-change-stream-wrapper.ts | 80 --------- .../src/deep-inspect-find-cursor-wrapper.ts | 156 ------------------ ...deep-inspect-run-command-cursor-wrapper.ts | 127 -------------- .../src/deep-inspect/cursor-wrapper.ts | 138 ++++++++++++++++ .../src/{ => deep-inspect}/custom-inspect.ts | 0 .../service-provider-wrapper.spec.ts} | 2 +- .../service-provider-wrapper.ts} | 17 +- .../ts-helpers.ts} | 4 + .../shell-api/src/shell-instance-state.ts | 2 +- 10 files changed, 151 insertions(+), 517 deletions(-) delete mode 100644 packages/shell-api/src/deep-inspect-aggregation-cursor-wrapper.ts delete mode 100644 packages/shell-api/src/deep-inspect-change-stream-wrapper.ts delete mode 100644 packages/shell-api/src/deep-inspect-find-cursor-wrapper.ts delete mode 100644 packages/shell-api/src/deep-inspect-run-command-cursor-wrapper.ts create mode 100644 packages/shell-api/src/deep-inspect/cursor-wrapper.ts rename packages/shell-api/src/{ => deep-inspect}/custom-inspect.ts (100%) rename packages/shell-api/src/{deep-inspect-service-provider-wrapper.spec.ts => deep-inspect/service-provider-wrapper.spec.ts} (99%) rename packages/shell-api/src/{deep-inspect-service-provider-wrapper.ts => deep-inspect/service-provider-wrapper.ts} (87%) rename packages/shell-api/src/{pick-methods-by-return-type.ts => deep-inspect/ts-helpers.ts} (53%) diff --git a/packages/shell-api/src/deep-inspect-aggregation-cursor-wrapper.ts b/packages/shell-api/src/deep-inspect-aggregation-cursor-wrapper.ts deleted file mode 100644 index 5e6dd67759..0000000000 --- a/packages/shell-api/src/deep-inspect-aggregation-cursor-wrapper.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { - Document, - ReadConcernLike, - ReadPreferenceLike, - ServiceProviderAggregationCursor, -} from '@mongosh/service-provider-core'; -import type { PickMethodsByReturnType } from './pick-methods-by-return-type'; -import { addCustomInspect } from './custom-inspect'; - -export class DeepInspectAggregationCursorWrapper - implements ServiceProviderAggregationCursor -{ - _cursor: ServiceProviderAggregationCursor; - - constructor(cursor: ServiceProviderAggregationCursor) { - this._cursor = cursor; - } - - project = forwardedMethod('project'); - skip = forwardedMethod('skip'); - sort = forwardedMethod('sort'); - explain = forwardedMethod('explain'); - addCursorFlag = forwardedMethod('addCursorFlag'); - withReadPreference = (readPreference: ReadPreferenceLike) => { - this._cursor.withReadPreference(readPreference); - return this; - }; - withReadConcern(readConcern: ReadConcernLike) { - this._cursor.withReadConcern(readConcern); - return this; - } - batchSize = forwardedMethod('batchSize'); - hasNext = forwardedMethod('hasNext'); - close = forwardedMethod('close'); - maxTimeMS = forwardedMethod('maxTimeMS'); - bufferedCount = forwardedMethod('bufferedCount'); - - next = forwardResultPromise('next'); - tryNext = forwardResultPromise('tryNext'); - - toArray = forwardResultsPromise('toArray'); - readBufferedDocuments = forwardResults( - 'readBufferedDocuments' - ); - - get closed(): boolean { - return this._cursor.closed; - } - - /*async *[Symbol.asyncIterator]() { - yield* this._cursor; - return; - }*/ -} - -function forwardResultPromise< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderAggregationCursor, - Promise - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return async function ( - this: DeepInspectAggregationCursorWrapper, - ...args: Parameters>[K]> - ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore The returntype already contains a promise - ReturnType>[K]> { - const result = await (this._cursor[key] as any)(...args); - if (result) { - addCustomInspect(result); - } - return result; - }; -} - -function forwardResultsPromise< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderAggregationCursor, - Promise - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return async function ( - this: DeepInspectAggregationCursorWrapper, - ...args: Parameters>[K]> - ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore The returntype already contains a promise - ReturnType>[K]> { - const results = await (this._cursor[key] as any)(...args); - addCustomInspect(results); - return results; - }; -} - -function forwardResults< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderAggregationCursor, - TSchema[] - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return function ( - this: DeepInspectAggregationCursorWrapper, - ...args: Parameters>[K]> - ): ReturnType>[K]> { - const results = (this._cursor[key] as any)(...args); - addCustomInspect(results); - return results; - }; -} - -function forwardedMethod< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderAggregationCursor, - any - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return function ( - this: DeepInspectAggregationCursorWrapper, - ...args: Parameters>[K]> - ): ReturnType>[K]> { - return (this._cursor[key] as any)(...args); - }; -} diff --git a/packages/shell-api/src/deep-inspect-change-stream-wrapper.ts b/packages/shell-api/src/deep-inspect-change-stream-wrapper.ts deleted file mode 100644 index c788ee69f8..0000000000 --- a/packages/shell-api/src/deep-inspect-change-stream-wrapper.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { - Document, - ResumeToken, - ServiceProviderChangeStream, -} from '@mongosh/service-provider-core'; -import type { PickMethodsByReturnType } from './pick-methods-by-return-type'; -import { addCustomInspect } from './custom-inspect'; - -export class DeepInspectChangeStreamWrapper - implements ServiceProviderChangeStream -{ - _cursor: ServiceProviderChangeStream; - - constructor(cursor: ServiceProviderChangeStream) { - this._cursor = cursor; - } - - get resumeToken(): ResumeToken { - return this._cursor.resumeToken; - } - - hasNext = forwardedMethod('hasNext'); - close = forwardedMethod('close'); - - next = forwardResultPromise('next'); - tryNext = forwardResultPromise('tryNext'); - - get closed(): boolean { - return this._cursor.closed; - } - - /*async *[Symbol.asyncIterator]() { - yield* this._cursor; - return; - }*/ -} - -function forwardResultPromise< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderChangeStream, - Promise - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return async function ( - this: DeepInspectChangeStreamWrapper, - ...args: Parameters>[K]> - ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore The returntype already contains a promise - ReturnType>[K]> { - const result = await (this._cursor[key] as any)(...args); - if (result) { - addCustomInspect(result); - } - return result; - }; -} - -function forwardedMethod< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderChangeStream, - any - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return function ( - this: DeepInspectChangeStreamWrapper, - ...args: Parameters>[K]> - ): ReturnType>[K]> { - return (this._cursor[key] as any)(...args); - }; -} diff --git a/packages/shell-api/src/deep-inspect-find-cursor-wrapper.ts b/packages/shell-api/src/deep-inspect-find-cursor-wrapper.ts deleted file mode 100644 index a0aff655e5..0000000000 --- a/packages/shell-api/src/deep-inspect-find-cursor-wrapper.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { - Document, - ReadConcernLike, - ReadPreferenceLike, - ServiceProviderFindCursor, -} from '@mongosh/service-provider-core'; -import type { PickMethodsByReturnType } from './pick-methods-by-return-type'; -import { addCustomInspect } from './custom-inspect'; - -export class DeepInspectFindCursorWrapper - implements ServiceProviderFindCursor -{ - _cursor: ServiceProviderFindCursor; - - constructor(cursor: ServiceProviderFindCursor) { - this._cursor = cursor; - } - - allowDiskUse = forwardedMethod('allowDiskUse'); - collation = forwardedMethod('collation'); - comment = forwardedMethod('comment'); - maxAwaitTimeMS = forwardedMethod('maxAwaitTimeMS'); - count = forwardedMethod('count'); - hint = forwardedMethod('hint'); - max = forwardedMethod('max'); - min = forwardedMethod('min'); - limit = forwardedMethod('limit'); - skip = forwardedMethod('skip'); - returnKey = forwardedMethod('returnKey'); - showRecordId = forwardedMethod('showRecordId'); - project = forwardedMethod('project'); - sort = forwardedMethod('sort'); - explain = forwardedMethod('explain'); - addCursorFlag = forwardedMethod('addCursorFlag'); - - withReadPreference = (readPreference: ReadPreferenceLike) => { - this._cursor.withReadPreference(readPreference); - return this; - }; - - withReadConcern(readConcern: ReadConcernLike) { - this._cursor.withReadConcern(readConcern); - return this; - } - - batchSize = forwardedMethod('batchSize'); - hasNext = forwardedMethod('hasNext'); - close = forwardedMethod('close'); - maxTimeMS = forwardedMethod('maxTimeMS'); - bufferedCount = forwardedMethod('bufferedCount'); - - next = forwardResultPromise('next'); - tryNext = forwardResultPromise('tryNext'); - - toArray = forwardResultsPromise('toArray'); - readBufferedDocuments = forwardResults( - 'readBufferedDocuments' - ); - - get closed(): boolean { - return this._cursor.closed; - } - - /*async *[Symbol.asyncIterator]() { - yield* this._cursor; - return; - }*/ -} - -function forwardResultPromise< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderFindCursor, - Promise - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return async function ( - this: DeepInspectFindCursorWrapper, - ...args: Parameters>[K]> - ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore The returntype already contains a promise - ReturnType>[K]> { - const result = await (this._cursor[key] as any)(...args); - if (result) { - addCustomInspect(result); - } - return result; - }; -} - -function forwardResultsPromise< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderFindCursor, - Promise - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return async function ( - this: DeepInspectFindCursorWrapper, - ...args: Parameters>[K]> - ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore The returntype already contains a promise - ReturnType>[K]> { - const results = await (this._cursor[key] as any)(...args); - addCustomInspect(results); - return results; - }; -} - -function forwardResults< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderFindCursor, - TSchema[] - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return function ( - this: DeepInspectFindCursorWrapper, - ...args: Parameters>[K]> - ): ReturnType>[K]> { - const results = (this._cursor[key] as any)(...args); - addCustomInspect(results); - return results; - }; -} - -function forwardedMethod< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderFindCursor, - any - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return function ( - this: DeepInspectFindCursorWrapper, - ...args: Parameters>[K]> - ): ReturnType>[K]> { - return (this._cursor[key] as any)(...args); - }; -} diff --git a/packages/shell-api/src/deep-inspect-run-command-cursor-wrapper.ts b/packages/shell-api/src/deep-inspect-run-command-cursor-wrapper.ts deleted file mode 100644 index 83f86c717d..0000000000 --- a/packages/shell-api/src/deep-inspect-run-command-cursor-wrapper.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { - Document, - ServiceProviderRunCommandCursor, -} from '@mongosh/service-provider-core'; -import type { PickMethodsByReturnType } from './pick-methods-by-return-type'; -import { addCustomInspect } from './custom-inspect'; - -export class DeepInspectRunCommandCursorWrapper - implements ServiceProviderRunCommandCursor -{ - _cursor: ServiceProviderRunCommandCursor; - - constructor(cursor: ServiceProviderRunCommandCursor) { - this._cursor = cursor; - } - - batchSize = forwardedMethod('batchSize'); - hasNext = forwardedMethod('hasNext'); - close = forwardedMethod('close'); - maxTimeMS = forwardedMethod('maxTimeMS'); - bufferedCount = forwardedMethod('bufferedCount'); - - next = forwardResultPromise('next'); - tryNext = forwardResultPromise('tryNext'); - - toArray = forwardResultsPromise('toArray'); - readBufferedDocuments = forwardResults( - 'readBufferedDocuments' - ); - - get closed(): boolean { - return this._cursor.closed; - } - - /*async *[Symbol.asyncIterator]() { - yield* this._cursor; - return; - }*/ -} - -function forwardResultPromise< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderRunCommandCursor, - Promise - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return async function ( - this: DeepInspectRunCommandCursorWrapper, - ...args: Parameters>[K]> - ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore The returntype already contains a promise - ReturnType>[K]> { - const result = await (this._cursor[key] as any)(...args); - if (result) { - addCustomInspect(result); - } - return result; - }; -} - -function forwardResultsPromise< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderRunCommandCursor, - Promise - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return async function ( - this: DeepInspectRunCommandCursorWrapper, - ...args: Parameters>[K]> - ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore The returntype already contains a promise - ReturnType>[K]> { - const results = await (this._cursor[key] as any)(...args); - addCustomInspect(results); - return results; - }; -} - -function forwardResults< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderRunCommandCursor, - TSchema[] - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return function ( - this: DeepInspectRunCommandCursorWrapper, - ...args: Parameters>[K]> - ): ReturnType>[K]> { - const results = (this._cursor[key] as any)(...args); - addCustomInspect(results); - return results; - }; -} - -function forwardedMethod< - TSchema, - K extends keyof PickMethodsByReturnType< - ServiceProviderRunCommandCursor, - any - > ->( - key: K -): ( - ...args: Parameters>[K]> -) => ReturnType>[K]> { - return function ( - this: DeepInspectRunCommandCursorWrapper, - ...args: Parameters>[K]> - ): ReturnType>[K]> { - return (this._cursor[key] as any)(...args); - }; -} diff --git a/packages/shell-api/src/deep-inspect/cursor-wrapper.ts b/packages/shell-api/src/deep-inspect/cursor-wrapper.ts new file mode 100644 index 0000000000..8a5b2cf2df --- /dev/null +++ b/packages/shell-api/src/deep-inspect/cursor-wrapper.ts @@ -0,0 +1,138 @@ +import type { + ReadConcernLike, + ReadPreferenceLike, + ServiceProviderFindCursor, + ServiceProviderBaseCursor, + ServiceProviderRunCommandCursor, + ServiceProviderAggregationCursor, + ServiceProviderChangeStream, + ResumeToken, +} from '@mongosh/service-provider-core'; +import type { + PickMethodsByReturnType, + UnionToIntersection, +} from './ts-helpers'; +import { addCustomInspect } from './custom-inspect'; + +type AnyCursor = + | ServiceProviderBaseCursor + | ServiceProviderRunCommandCursor + | ServiceProviderFindCursor + | ServiceProviderAggregationCursor + | ServiceProviderChangeStream; +type AllCursor = UnionToIntersection>; + +export function deepInspectCursorWrapper< + TSchema, + Cursor extends AnyCursor +>(_cursor: Cursor): Cursor { + // All methods are potentially defined on the union + const cursor = _cursor as Cursor & Partial>; + return { + allowDiskUse: forwardedMethod('allowDiskUse', cursor), + collation: forwardedMethod('collation', cursor), + comment: forwardedMethod('comment', cursor), + maxAwaitTimeMS: forwardedMethod('maxAwaitTimeMS', cursor), + count: forwardedMethod('count', cursor), + hint: forwardedMethod('hint', cursor), + max: forwardedMethod('max', cursor), + min: forwardedMethod('min', cursor), + limit: forwardedMethod('limit', cursor), + skip: forwardedMethod('skip', cursor), + returnKey: forwardedMethod('returnKey', cursor), + showRecordId: forwardedMethod('showRecordId', cursor), + project: forwardedMethod('project', cursor), + sort: forwardedMethod('sort', cursor), + explain: forwardedMethod('explain', cursor), + addCursorFlag: forwardedMethod('addCursorFlag', cursor), + + withReadPreference: cursor.withReadPreference + ? (readPreference: ReadPreferenceLike) => { + cursor.withReadPreference!(readPreference); + return cursor; + } + : undefined, + + withReadConcern: cursor.withReadConcern + ? (readConcern: ReadConcernLike) => { + cursor.withReadConcern!(readConcern); + return cursor; + } + : undefined, + + batchSize: forwardedMethod('batchSize', cursor), + hasNext: forwardedMethod('hasNext', cursor), + close: forwardedMethod('close', cursor), + maxTimeMS: forwardedMethod('maxTimeMS', cursor), + bufferedCount: forwardedMethod('bufferedCount', cursor), + + next: forwardResultPromise('next', cursor), + tryNext: forwardResultPromise('tryNext', cursor), + + toArray: forwardResultPromise('toArray', cursor), + readBufferedDocuments: forwardResults('readBufferedDocuments', cursor), + + get closed(): boolean { + return cursor.closed; + }, + + get resumeToken(): ResumeToken { + return cursor.resumeToken; + }, + + [Symbol.asyncIterator]: cursor[Symbol.asyncIterator] + ? async function* (): AsyncGenerator { + for await (const doc of cursor[Symbol.asyncIterator]!()) { + addCustomInspect(doc); + yield doc; + } + } + : undefined, + } satisfies Record< + keyof AllCursor, + unknown + > as AnyCursor as Cursor; +} + +function forwardResultPromise< + TSchema, + Cursor extends AnyCursor, + K extends keyof PickMethodsByReturnType> +>(key: K, cursor: Cursor): Cursor[K] { + if (!cursor[key]) return undefined as Cursor[K]; + return async function ( + ...args: any[] + ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore The returntype already contains a promise + ReturnType[K]> { + const result = await (cursor[key] as any)(...args); + if (result) { + addCustomInspect(result); + } + return result; + } as Cursor[K]; +} + +function forwardResults< + TSchema, + Cursor extends AnyCursor, + K extends keyof PickMethodsByReturnType +>(key: K, cursor: Cursor): Cursor[K] { + if (!cursor[key]) return undefined as Cursor[K]; + return function (...args: any[]) { + const results = (cursor[key] as any)(...args); + addCustomInspect(results); + return results; + } as Cursor[K]; +} + +function forwardedMethod< + TSchema, + Cursor extends AnyCursor, + K extends keyof Cursor +>(key: K, cursor: Cursor): Cursor[K] { + if (!cursor[key]) return undefined as Cursor[K]; + return function (...args: any[]) { + return (cursor[key] as any)(...args); + } as Cursor[K]; +} diff --git a/packages/shell-api/src/custom-inspect.ts b/packages/shell-api/src/deep-inspect/custom-inspect.ts similarity index 100% rename from packages/shell-api/src/custom-inspect.ts rename to packages/shell-api/src/deep-inspect/custom-inspect.ts diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts b/packages/shell-api/src/deep-inspect/service-provider-wrapper.spec.ts similarity index 99% rename from packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts rename to packages/shell-api/src/deep-inspect/service-provider-wrapper.spec.ts index 5fbccdba13..ca2c7d15d8 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.spec.ts +++ b/packages/shell-api/src/deep-inspect/service-provider-wrapper.spec.ts @@ -6,7 +6,7 @@ import sinonChai from 'sinon-chai'; import sinon from 'sinon'; import type { StubbedInstance } from 'ts-sinon'; import { stubInterface } from 'ts-sinon'; -import { deepInspectServiceProviderWrapper } from './deep-inspect-service-provider-wrapper'; +import { deepInspectServiceProviderWrapper } from './service-provider-wrapper'; import * as util from 'util'; import { makePrintableBson } from '@mongosh/shell-bson'; diff --git a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect/service-provider-wrapper.ts similarity index 87% rename from packages/shell-api/src/deep-inspect-service-provider-wrapper.ts rename to packages/shell-api/src/deep-inspect/service-provider-wrapper.ts index 7f4afb1bcd..cee45e4c16 100644 --- a/packages/shell-api/src/deep-inspect-service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect/service-provider-wrapper.ts @@ -1,10 +1,7 @@ import type { ServiceProvider } from '@mongosh/service-provider-core'; -import { DeepInspectAggregationCursorWrapper } from './deep-inspect-aggregation-cursor-wrapper'; -import { DeepInspectFindCursorWrapper } from './deep-inspect-find-cursor-wrapper'; +import { deepInspectCursorWrapper } from './cursor-wrapper'; import { addCustomInspect } from './custom-inspect'; -import type { PickMethodsByReturnType } from './pick-methods-by-return-type'; -import { DeepInspectRunCommandCursorWrapper } from './deep-inspect-run-command-cursor-wrapper'; -import { DeepInspectChangeStreamWrapper } from './deep-inspect-change-stream-wrapper'; +import type { PickMethodsByReturnType } from './ts-helpers'; export function deepInspectServiceProviderWrapper( sp: ServiceProvider @@ -15,11 +12,11 @@ export function deepInspectServiceProviderWrapper( }, aggregate: (...args: Parameters) => { const cursor = sp.aggregate(...args); - return new DeepInspectAggregationCursorWrapper(cursor); + return deepInspectCursorWrapper(cursor); }, aggregateDb: (...args: Parameters) => { const cursor = sp.aggregateDb(...args); - return new DeepInspectAggregationCursorWrapper(cursor); + return deepInspectCursorWrapper(cursor); }, count: forwardedMethod('count', sp), estimatedDocumentCount: forwardedMethod('estimatedDocumentCount', sp), @@ -27,7 +24,7 @@ export function deepInspectServiceProviderWrapper( distinct: bsonMethod('distinct', sp), find: (...args: Parameters) => { const cursor = sp.find(...args); - return new DeepInspectFindCursorWrapper(cursor); + return deepInspectCursorWrapper(cursor); }, findOneAndDelete: bsonMethod('findOneAndDelete', sp), findOneAndReplace: bsonMethod('findOneAndReplace', sp), @@ -38,7 +35,7 @@ export function deepInspectServiceProviderWrapper( readPreferenceFromOptions: forwardedMethod('readPreferenceFromOptions', sp), watch: (...args: Parameters) => { const cursor = sp.watch(...args); - return new DeepInspectChangeStreamWrapper(cursor); + return deepInspectCursorWrapper(cursor); }, getSearchIndexes: bsonMethod('getSearchIndexes', sp), runCommand: bsonMethod('runCommand', sp), @@ -47,7 +44,7 @@ export function deepInspectServiceProviderWrapper( ...args: Parameters ) => { const cursor = sp.runCursorCommand(...args); - return new DeepInspectRunCommandCursorWrapper(cursor); + return deepInspectCursorWrapper(cursor); }, dropDatabase: bsonMethod('dropDatabase', sp), dropCollection: forwardedMethod('dropCollection', sp), diff --git a/packages/shell-api/src/pick-methods-by-return-type.ts b/packages/shell-api/src/deep-inspect/ts-helpers.ts similarity index 53% rename from packages/shell-api/src/pick-methods-by-return-type.ts rename to packages/shell-api/src/deep-inspect/ts-helpers.ts index 20e8cf2050..bda1497eff 100644 --- a/packages/shell-api/src/pick-methods-by-return-type.ts +++ b/packages/shell-api/src/deep-inspect/ts-helpers.ts @@ -1,3 +1,7 @@ + +export type UnionToIntersection = + (T extends any ? (k: T) => void : never) extends ((k: infer U) => void) ? U : never + export type PickMethodsByReturnType = { [k in keyof T as NonNullable extends (...args: any[]) => R ? k diff --git a/packages/shell-api/src/shell-instance-state.ts b/packages/shell-api/src/shell-instance-state.ts index 651a5008d7..9e3e948035 100644 --- a/packages/shell-api/src/shell-instance-state.ts +++ b/packages/shell-api/src/shell-instance-state.ts @@ -51,7 +51,7 @@ import type { AutocompletionContext } from '@mongodb-js/mongodb-ts-autocomplete' import type { JSONSchema } from 'mongodb-schema'; import { analyzeDocuments } from 'mongodb-schema'; import type { BaseCursor } from './abstract-cursor'; -import { deepInspectServiceProviderWrapper } from './deep-inspect-service-provider-wrapper'; +import { deepInspectServiceProviderWrapper } from './deep-inspect/service-provider-wrapper'; /** * The subset of CLI options that is relevant for the shell API's behavior itself. From 2bc07c3cd40e4ca0304465784e13b511d707ed06 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 27 Nov 2025 18:03:13 +0100 Subject: [PATCH 26/33] fixup: fix runtime independence test again --- packages/shell-api/src/deep-inspect/cursor-wrapper.ts | 3 +++ .../shell-api/src/deep-inspect/service-provider-wrapper.ts | 3 +++ packages/shell-api/src/runtime-independence.spec.ts | 6 +++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/shell-api/src/deep-inspect/cursor-wrapper.ts b/packages/shell-api/src/deep-inspect/cursor-wrapper.ts index 8a5b2cf2df..eab44a6c05 100644 --- a/packages/shell-api/src/deep-inspect/cursor-wrapper.ts +++ b/packages/shell-api/src/deep-inspect/cursor-wrapper.ts @@ -29,6 +29,9 @@ export function deepInspectCursorWrapper< // All methods are potentially defined on the union const cursor = _cursor as Cursor & Partial>; return { + get [Symbol.for('@@mongosh.originalCursor')]() { + return cursor; + }, allowDiskUse: forwardedMethod('allowDiskUse', cursor), collation: forwardedMethod('collation', cursor), comment: forwardedMethod('comment', cursor), diff --git a/packages/shell-api/src/deep-inspect/service-provider-wrapper.ts b/packages/shell-api/src/deep-inspect/service-provider-wrapper.ts index cee45e4c16..acb3a8e4c8 100644 --- a/packages/shell-api/src/deep-inspect/service-provider-wrapper.ts +++ b/packages/shell-api/src/deep-inspect/service-provider-wrapper.ts @@ -7,6 +7,9 @@ export function deepInspectServiceProviderWrapper( sp: ServiceProvider ): ServiceProvider { return { + get [Symbol.for('@@mongosh.originalServiceProvider')]() { + return sp; + }, get bsonLibrary() { return sp.bsonLibrary; }, diff --git a/packages/shell-api/src/runtime-independence.spec.ts b/packages/shell-api/src/runtime-independence.spec.ts index a150a987be..a6e99c1e7a 100644 --- a/packages/shell-api/src/runtime-independence.spec.ts +++ b/packages/shell-api/src/runtime-independence.spec.ts @@ -65,7 +65,11 @@ describe('Runtime independence', function () { const evaluationListener = { onExit: sinon.spy() }; const instanceState = new shellApi.ShellInstanceState(sp as any); instanceState.setEvaluationListener(evaluationListener); - expect((instanceState.initialServiceProvider as any)._sp).to.equal(sp); + expect( + (instanceState.initialServiceProvider as any)[ + Symbol.for('@@mongosh.originalServiceProvider') + ] + ).to.equal(sp); const bsonObj = instanceState.shellBson.ISODate( '2025-01-09T20:43:51+01:00' ); From 370288fad1ee2ca0265e1a3100a5d1d160f034f8 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 27 Nov 2025 18:37:21 +0100 Subject: [PATCH 27/33] fixup: add e2e tests --- packages/cli-repl/src/format-output.ts | 3 ++ packages/e2e-tests/test/e2e-bson.spec.ts | 49 ++++++++++++++++++++++++ packages/e2e-tests/test/test-shell.ts | 7 +++- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/cli-repl/src/format-output.ts b/packages/cli-repl/src/format-output.ts index bd6971aa4a..2f2aac7d91 100644 --- a/packages/cli-repl/src/format-output.ts +++ b/packages/cli-repl/src/format-output.ts @@ -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( @@ -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; } } diff --git a/packages/e2e-tests/test/e2e-bson.spec.ts b/packages/e2e-tests/test/e2e-bson.spec.ts index 2299bc4683..b207a56edb 100644 --- a/packages/e2e-tests/test/e2e-bson.spec.ts +++ b/packages/e2e-tests/test/e2e-bson.spec.ts @@ -588,4 +588,53 @@ describe('BSON e2e', function () { shell.assertNoErrors(); }); }); + describe('inspect nesting depth', function () { + it('inspects a full bson document when it is read from the server', async function () { + await shell.executeLine(`use ${dbName}`); + await shell.executeLine(`deepAndNested = ({ + a: { b: { c: { d: { e: { f: { g: { h: "foundme" } } } } } } }, + array: [...Array(100000).keys()].map(i => ({ num: i })), + str: 'All work and no play makes Jack a dull boy'.repeat(4096) + 'The End' + });`); + await shell.executeLine(`db.coll.insertOne(deepAndNested)`); + // Deeply nested object from the server should be fully printed + const output = await shell.executeLine('db.coll.findOne()'); + 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'); + // Same object doesn't need to be fully printed if created by the user + const output2 = await shell.executeLine('deepAndNested'); + expect(output2).to.include('[Object'); + expect(output2).to.include('more items'); + expect(output2).not.to.include('foundme'); + expect(output2).not.to.include('num: 99999'); + expect(output2).not.to.include('The End'); + shell.assertNoErrors(); + }); + 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(); + }); + }); }); diff --git a/packages/e2e-tests/test/test-shell.ts b/packages/e2e-tests/test/test-shell.ts index f3a310ae2e..f757145ea9 100644 --- a/packages/e2e-tests/test/test-shell.ts +++ b/packages/e2e-tests/test/test-shell.ts @@ -302,14 +302,17 @@ export class TestShell { return this._output.slice(previousOutputLength); } - async executeLineWithJSONResult(line: string): Promise { + async executeLineWithJSONResult( + line: string, + { parseAsEJSON = true } = {} + ): Promise { const output = await this.executeLine( `">>>>>>" + EJSON.stringify(${line}, {relaxed:false}) + "<<<<<<"` ); const matching = output.match(/>>>>>>(.+)<<<<< Date: Thu, 27 Nov 2025 19:25:37 +0100 Subject: [PATCH 28/33] fixup: oidc test --- packages/e2e-tests/test/e2e-oidc.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-tests/test/e2e-oidc.spec.ts b/packages/e2e-tests/test/e2e-oidc.spec.ts index 50964a0fba..48cc5ed19a 100644 --- a/packages/e2e-tests/test/e2e-oidc.spec.ts +++ b/packages/e2e-tests/test/e2e-oidc.spec.ts @@ -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._sp.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. From 7202eb2f0498e6a338c685cbe0b3639255a031e6 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 28 Nov 2025 11:15:39 +0100 Subject: [PATCH 29/33] fixup: stub out more cli-repl test sp usage --- packages/cli-repl/src/mongosh-repl.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli-repl/src/mongosh-repl.spec.ts b/packages/cli-repl/src/mongosh-repl.spec.ts index 5852e746e8..c226bc018e 100644 --- a/packages/cli-repl/src/mongosh-repl.spec.ts +++ b/packages/cli-repl/src/mongosh-repl.spec.ts @@ -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, @@ -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' }]); From 1b72c5b4cd4d503aab4944dfdf88c4660cccaaf1 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 28 Nov 2025 16:07:52 +0100 Subject: [PATCH 30/33] feat(cli-repl): add flag to control deep inspect behavior --- packages/arg-parser/src/cli-options.ts | 1 + packages/cli-repl/src/arg-parser.ts | 1 + packages/cli-repl/src/cli-repl.ts | 11 +- packages/cli-repl/src/constants.ts | 3 + packages/cli-repl/src/format-output.ts | 2 +- packages/cli-repl/src/mongosh-repl.ts | 5 +- packages/e2e-tests/test/e2e-bson.spec.ts | 130 +++++++++++++++--- packages/i18n/src/locales/en_US.ts | 2 + .../shell-api/src/shell-instance-state.ts | 8 +- 9 files changed, 139 insertions(+), 24 deletions(-) diff --git a/packages/arg-parser/src/cli-options.ts b/packages/arg-parser/src/cli-options.ts index 8238cb504e..0dafc7bc53 100644 --- a/packages/arg-parser/src/cli-options.ts +++ b/packages/arg-parser/src/cli-options.ts @@ -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; diff --git a/packages/cli-repl/src/arg-parser.ts b/packages/cli-repl/src/arg-parser.ts index 932fae1d46..73dce856e5 100644 --- a/packages/cli-repl/src/arg-parser.ts +++ b/packages/cli-repl/src/arg-parser.ts @@ -58,6 +58,7 @@ const OPTIONS = { 'apiDeprecationErrors', 'apiStrict', 'buildInfo', + 'deepInspect', 'exposeAsyncRewriter', 'help', 'ipv6', diff --git a/packages/cli-repl/src/cli-repl.ts b/packages/cli-repl/src/cli-repl.ts index 7c24961ff1..0a5a571fc9 100644 --- a/packages/cli-repl/src/cli-repl.ts +++ b/packages/cli-repl/src/cli-repl.ts @@ -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. @@ -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, }, @@ -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'); } diff --git a/packages/cli-repl/src/constants.ts b/packages/cli-repl/src/constants.ts index 537757ff14..523882c21e 100644 --- a/packages/cli-repl/src/constants.ts +++ b/packages/cli-repl/src/constants.ts @@ -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'), diff --git a/packages/cli-repl/src/format-output.ts b/packages/cli-repl/src/format-output.ts index 2f2aac7d91..4e57e93c12 100644 --- a/packages/cli-repl/src/format-output.ts +++ b/packages/cli-repl/src/format-output.ts @@ -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, diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index cd2d7a5934..30a4b341fd 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -955,7 +955,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): string { // This checks for error instances. // The writer gets called immediately by the internal `repl.eval` // in case of errors. @@ -975,7 +975,8 @@ class MongoshNodeRepl implements EvaluationListener { this.rawValueToShellResult.get(result) ?? { type: null, printable: result, - } + }, + extraFormatOptions ); } diff --git a/packages/e2e-tests/test/e2e-bson.spec.ts b/packages/e2e-tests/test/e2e-bson.spec.ts index b207a56edb..9eafc42e21 100644 --- a/packages/e2e-tests/test/e2e-bson.spec.ts +++ b/packages/e2e-tests/test/e2e-bson.spec.ts @@ -589,30 +589,128 @@ describe('BSON e2e', function () { }); }); describe('inspect nesting depth', function () { - it('inspects a full bson document when it is read from the server', async 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 = ({ - a: { b: { c: { d: { e: { f: { g: { h: "foundme" } } } } } } }, - array: [...Array(100000).keys()].map(i => ({ num: i })), - str: 'All work and no play makes Jack a dull boy'.repeat(4096) + 'The End' - });`); + 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()'); - 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'); + checkForDeepOutput(output, true); // Same object doesn't need to be fully printed if created by the user const output2 = await shell.executeLine('deepAndNested'); - expect(output2).to.include('[Object'); - expect(output2).to.include('more items'); - expect(output2).not.to.include('foundme'); - expect(output2).not.to.include('num: 99999'); - expect(output2).not.to.include('The End'); + 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.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`, + ], + }); + await shell.waitForSuccessfulExit(); + checkForDeepOutput(shell.output, false); + shell.assertNoErrors(); + shell = this.startTestShell({ + args: [ + await testServer.connectionString(), + '--eval', + `print(${deepAndNestedDefinition}); 0`, + ], + }); + await shell.waitForSuccessfulExit(); + checkForDeepOutput(shell.output, false); + shell.assertNoErrors(); + }); + + 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();`, + ], + }); + await shell.waitForSuccessfulExit(); + checkForDeepOutput(shell.output, true); + shell.assertNoErrors(); + shell = this.startTestShell({ + args: [ + await testServer.connectionString(), + '--eval', + deepAndNestedDefinition, + ], + }); + await shell.waitForSuccessfulExit(); + checkForDeepOutput(shell.output, true); shell.assertNoErrors(); }); + + 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();`, + ], + }); + await shell.waitForSuccessfulExit(); + checkForDeepOutput(shell.output, false); + shell.assertNoErrors(); + shell = this.startTestShell({ + args: [ + await testServer.connectionString(), + '--deepInspect=false', + '--eval', + deepAndNestedDefinition, + ], + }); + await shell.waitForSuccessfulExit(); + checkForDeepOutput(shell.output, false); + shell.assertNoErrors(); + }); + 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 diff --git a/packages/i18n/src/locales/en_US.ts b/packages/i18n/src/locales/en_US.ts index 0016b54217..50100f189a 100644 --- a/packages/i18n/src/locales/en_US.ts +++ b/packages/i18n/src/locales/en_US.ts @@ -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', diff --git a/packages/shell-api/src/shell-instance-state.ts b/packages/shell-api/src/shell-instance-state.ts index 9e3e948035..03d98613d3 100644 --- a/packages/shell-api/src/shell-instance-state.ts +++ b/packages/shell-api/src/shell-instance-state.ts @@ -58,6 +58,7 @@ import { deepInspectServiceProviderWrapper } from './deep-inspect/service-provid */ export interface ShellCliOptions { nodb?: boolean; + deepInspect?: boolean; } /** @@ -204,9 +205,10 @@ export class ShellInstanceState { cliOptions: ShellCliOptions = {}, bsonLibrary: BSONLibrary = initialServiceProvider.bsonLibrary ) { - this.initialServiceProvider = deepInspectServiceProviderWrapper( - initialServiceProvider - ); + this.initialServiceProvider = + cliOptions.deepInspect === false + ? initialServiceProvider + : deepInspectServiceProviderWrapper(initialServiceProvider); this.bsonLibrary = bsonLibrary; this.messageBus = messageBus; this.shellApi = new ShellApi(this); From f783da404cb452751af36d4c1daade13408c56a1 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 28 Nov 2025 16:10:25 +0100 Subject: [PATCH 31/33] fixup: prettier doesnt run on subdirs? --- packages/shell-api/src/deep-inspect/ts-helpers.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/shell-api/src/deep-inspect/ts-helpers.ts b/packages/shell-api/src/deep-inspect/ts-helpers.ts index bda1497eff..582e829d16 100644 --- a/packages/shell-api/src/deep-inspect/ts-helpers.ts +++ b/packages/shell-api/src/deep-inspect/ts-helpers.ts @@ -1,6 +1,8 @@ - -export type UnionToIntersection = - (T extends any ? (k: T) => void : never) extends ((k: infer U) => void) ? U : never +export type UnionToIntersection = ( + T extends any ? (k: T) => void : never +) extends (k: infer U) => void + ? U + : never; export type PickMethodsByReturnType = { [k in keyof T as NonNullable extends (...args: any[]) => R From 0a9c1bf9ebf55f27e244e37ed80f375c9e136b9f Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 28 Nov 2025 17:03:36 +0100 Subject: [PATCH 32/33] fixup: add missing waitForPrompt call --- packages/e2e-tests/test/e2e-bson.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/e2e-tests/test/e2e-bson.spec.ts b/packages/e2e-tests/test/e2e-bson.spec.ts index 9eafc42e21..71f5a212c8 100644 --- a/packages/e2e-tests/test/e2e-bson.spec.ts +++ b/packages/e2e-tests/test/e2e-bson.spec.ts @@ -631,6 +631,7 @@ describe('BSON e2e', function () { 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); From caaa723bf0f7a276255807bd272ffdfd4fb6c4ec Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 28 Nov 2025 17:07:40 +0100 Subject: [PATCH 33/33] fixup: simplify using waitForCleanOutput --- packages/e2e-tests/test/e2e-bson.spec.ts | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/e2e-tests/test/e2e-bson.spec.ts b/packages/e2e-tests/test/e2e-bson.spec.ts index 71f5a212c8..ca84f46575 100644 --- a/packages/e2e-tests/test/e2e-bson.spec.ts +++ b/packages/e2e-tests/test/e2e-bson.spec.ts @@ -647,9 +647,7 @@ describe('BSON e2e', function () { `use(${JSON.stringify(dbName)}); print(db.coll.findOne()); 0`, ], }); - await shell.waitForSuccessfulExit(); - checkForDeepOutput(shell.output, false); - shell.assertNoErrors(); + checkForDeepOutput(await shell.waitForCleanOutput(), false); shell = this.startTestShell({ args: [ await testServer.connectionString(), @@ -657,9 +655,7 @@ describe('BSON e2e', function () { `print(${deepAndNestedDefinition}); 0`, ], }); - await shell.waitForSuccessfulExit(); - checkForDeepOutput(shell.output, false); - shell.assertNoErrors(); + checkForDeepOutput(await shell.waitForCleanOutput(), false); }); it('inspect full objects in non-interactive mode for final output', async function () { @@ -671,9 +667,7 @@ describe('BSON e2e', function () { `use(${JSON.stringify(dbName)}); db.coll.findOne();`, ], }); - await shell.waitForSuccessfulExit(); - checkForDeepOutput(shell.output, true); - shell.assertNoErrors(); + checkForDeepOutput(await shell.waitForCleanOutput(), true); shell = this.startTestShell({ args: [ await testServer.connectionString(), @@ -681,9 +675,7 @@ describe('BSON e2e', function () { deepAndNestedDefinition, ], }); - await shell.waitForSuccessfulExit(); - checkForDeepOutput(shell.output, true); - shell.assertNoErrors(); + checkForDeepOutput(await shell.waitForCleanOutput(), true); }); it('can explicitly disable full-depth nesting (non-interactive mode)', async function () { @@ -696,9 +688,7 @@ describe('BSON e2e', function () { `use(${JSON.stringify(dbName)}); db.coll.findOne();`, ], }); - await shell.waitForSuccessfulExit(); - checkForDeepOutput(shell.output, false); - shell.assertNoErrors(); + checkForDeepOutput(await shell.waitForCleanOutput(), false); shell = this.startTestShell({ args: [ await testServer.connectionString(), @@ -707,9 +697,7 @@ describe('BSON e2e', function () { deepAndNestedDefinition, ], }); - await shell.waitForSuccessfulExit(); - checkForDeepOutput(shell.output, false); - shell.assertNoErrors(); + checkForDeepOutput(await shell.waitForCleanOutput(), false); }); it('can parse serverStatus back to its original form', async function () {