From 40a87277bbc711b9768e54997e63a19e9e822ef5 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 3 Jun 2024 11:00:27 +0200 Subject: [PATCH 01/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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);