Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
40a8727
WIP
addaleax Jun 3, 2024
4b2991c
use DeepInspectServiceProviderWrapper, install our own inspect functi…
lerouxb Nov 20, 2025
f85dc9f
you cannot extend the return value of initializeBulkOp
lerouxb Nov 20, 2025
24ef043
apparently inspect passed as the final parameter isn't always a thing
lerouxb Nov 20, 2025
cda077c
better name
lerouxb Nov 24, 2025
7d008f7
inspect a shallow copy
lerouxb Nov 24, 2025
a27b349
fix comment
lerouxb Nov 24, 2025
8c523bb
leave bson values alone, don't accidentally override existing custom …
lerouxb Nov 24, 2025
aa2e830
don't remove things
lerouxb Nov 24, 2025
6307dd3
wrap every type of cursor
lerouxb Nov 24, 2025
70aa3ee
don't accidentally override our custom Date and RegExp inpect functions
lerouxb Nov 24, 2025
8624b68
don't depend on inspect
lerouxb Nov 24, 2025
cae4017
more indirection
lerouxb Nov 24, 2025
7d2d0a3
fill out more things on the stub
lerouxb Nov 24, 2025
d12548c
how did this work before?
lerouxb Nov 24, 2025
12fc59b
Merge branch 'main' into print-output-full
lerouxb Nov 25, 2025
22e67f8
add the custom inspect symbol as not-enumerable
lerouxb Nov 25, 2025
f4f64bc
pull bsonLibrary off _sp rather
lerouxb Nov 25, 2025
709de79
Merge branch 'main' into print-output-full
lerouxb Nov 26, 2025
77f64f2
don't wrap the service provider in java land
lerouxb Nov 27, 2025
72d8679
Update packages/shell-api/src/custom-inspect.ts
lerouxb Nov 27, 2025
a93b8fd
some unit tests
lerouxb Nov 27, 2025
0ce95c3
more unit tests
lerouxb Nov 27, 2025
69d3d13
adjust runtime indepdendence tests
lerouxb Nov 27, 2025
209c9f5
Update packages/shell-api/src/custom-inspect.ts
lerouxb Nov 27, 2025
c023d46
fixup: remove wrappable flag, skip async iter support for now, use fn…
addaleax Nov 27, 2025
e31ba35
fixup: merge cursor implementations, move to separate subdir
addaleax Nov 27, 2025
2bc07c3
fixup: fix runtime independence test again
addaleax Nov 27, 2025
370288f
fixup: add e2e tests
addaleax Nov 27, 2025
28dd205
fixup: oidc test
addaleax Nov 27, 2025
7202eb2
fixup: stub out more cli-repl test sp usage
addaleax Nov 28, 2025
1b72c5b
feat(cli-repl): add flag to control deep inspect behavior
addaleax Nov 28, 2025
f783da4
fixup: prettier doesnt run on subdirs?
addaleax Nov 28, 2025
0a9c1bf
fixup: add missing waitForPrompt call
addaleax Nov 28, 2025
caaa723
fixup: simplify using waitForCleanOutput
addaleax Nov 28, 2025
02e70c7
Merge remote-tracking branch 'origin/main' into print-output-full
addaleax Nov 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/service-provider-core/src/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
AutoEncryptionOptions,
Collection,
} from './all-transport-types';
import type { ConnectionExtraInfo } from './index';
import type { ConnectionExtraInfo, ServiceProvider } from './index';
import type { ReplPlatform } from './platform';
import type {
AWSEncryptionKeyOptions,
Expand Down Expand Up @@ -90,7 +90,10 @@ export default interface Admin {
* @param uri
* @param options
*/
getNewConnection(uri: string, options: MongoClientOptions): Promise<any>; // returns the ServiceProvider instance
getNewConnection(
uri: string,
options: MongoClientOptions
): Promise<ServiceProvider>;

/**
* Return the URI for the current connection, if this ServiceProvider is connected.
Expand Down
7 changes: 7 additions & 0 deletions packages/service-provider-core/src/cursors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,10 @@ export interface ServiceProviderChangeStream<TSchema = Document>
next(): Promise<TSchema>;
readonly resumeToken: ResumeToken;
}

export type ServiceProviderAnyCursor<TSchema = Document> =
| ServiceProviderAggregationCursor<TSchema>
| ServiceProviderFindCursor<TSchema>
| ServiceProviderRunCommandCursor<TSchema>
| ServiceProviderFindCursor<TSchema>
| ServiceProviderChangeStream<TSchema>;
1 change: 1 addition & 0 deletions packages/service-provider-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export {
ServiceProviderFindCursor,
ServiceProviderRunCommandCursor,
ServiceProviderChangeStream,
ServiceProviderAnyCursor,
} from './cursors';

export {
Expand Down
257 changes: 257 additions & 0 deletions packages/shell-api/src/deep-inspect-service-provider-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import type {
ServiceProvider,
ServiceProviderAbstractCursor,
} 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';

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 = forwardedMethod('count');
estimatedDocumentCount = forwardedMethod('estimatedDocumentCount');
countDocuments = forwardedMethod('countDocuments');
distinct = bsonMethod('distinct');
find = cursorMethod('find');
findOneAndDelete = bsonMethod('findOneAndDelete');
findOneAndReplace = bsonMethod('findOneAndReplace');
findOneAndUpdate = bsonMethod('findOneAndUpdate');
getTopologyDescription = forwardedMethod('getTopologyDescription');
getIndexes = bsonMethod('getIndexes');
listCollections = bsonMethod('listCollections');
readPreferenceFromOptions = forwardedMethod('readPreferenceFromOptions');
// 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 = 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<ServiceProvider['getNewConnection']>
): Promise<ServiceProvider> {
const sp = await this._sp.getNewConnection(...args);
return new DeepInspectServiceProviderWrapper(sp as ServiceProvider);
}
}

type PickMethodsByReturnType<T, R> = {
[k in keyof T as NonNullable<T[k]> extends (...args: any[]) => R
? k
: never]: T[k];
};

function cursorMethod<
K extends keyof PickMethodsByReturnType<
ServiceProvider,
ServiceProviderAbstractCursor
>
>(
key: K
): (
...args: Parameters<Required<ServiceProvider>[K]>
) => ReturnType<Required<ServiceProvider>[K]> {
return function (
this: DeepInspectServiceProviderWrapper,
...args: Parameters<ServiceProvider[K]>
): ReturnType<ServiceProvider[K]> {
// The problem here is that ReturnType<ServiceProvider[K]> 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<Document | null>
);
cursor.tryNext = cursorTryNext(
cursor.tryNext.bind(cursor) as () => Promise<Document | null>
);

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<Document[]>
);
}

return cursor;
};
}

const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');

function cursorNext(
original: () => Promise<Document | null>
): () => Promise<Document | null> {
return async function (): Promise<Document | null> {
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<Document[]>
): () => Promise<Document[]> {
return async function (): Promise<Document[]> {
const results = await original();

replaceWithCustomInspect(results);

return results;
};
}

function bsonMethod<
K extends keyof PickMethodsByReturnType<ServiceProvider, Promise<any>>
>(
key: K
): (
...args: Parameters<Required<ServiceProvider>[K]>
) => ReturnType<Required<ServiceProvider>[K]> {
return async function (
this: DeepInspectServiceProviderWrapper,
...args: Parameters<Required<ServiceProvider>[K]>
): // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore The returntype already contains a promise
ReturnType<Required<ServiceProvider>[K]> {
const result = await (this._sp[key] as any)(...args);
replaceWithCustomInspect(result);
return result;
};
}

function forwardedMethod<
K extends keyof PickMethodsByReturnType<ServiceProvider, any>
>(
key: K
): (
...args: Parameters<Required<ServiceProvider>[K]>
) => ReturnType<Required<ServiceProvider>[K]> {
return function (
this: DeepInspectServiceProviderWrapper,
...args: Parameters<Required<ServiceProvider>[K]>
): ReturnType<Required<ServiceProvider>[K]> {
// 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
) {
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);
}
}
}
9 changes: 6 additions & 3 deletions packages/shell-api/src/shell-instance-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -220,11 +223,11 @@ export class ShellInstanceState {
undefined,
undefined,
undefined,
initialServiceProvider
this.initialServiceProvider
Copy link
Contributor Author

Choose a reason for hiding this comment

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

btw it is VERY easy to accidentally pass initialServiceProvider (ie. the unwrapped value) to something in place of this.initialServiceProvider. Ask me how I know..

);
this.mongos.push(mongo);
this.currentDb = mongo.getDB(
initialServiceProvider.initialDb || DEFAULT_DB
this.initialServiceProvider.initialDb || DEFAULT_DB
);
} else {
this.currentDb = new NoDatabase() as DatabaseWithSchema;
Expand Down
Loading