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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/en_US.js
Original file line number Diff line number Diff line change
Expand Up @@ -1603,6 +1603,13 @@ const translations = {
}
}
}
},
Document: {
help: {
link: 'https://docs.mongodb.com/manual/core/document/',
description: 'A generic MongoDB document, without any methods.',
attributes: {}
}
}
}
},
Expand Down
6 changes: 4 additions & 2 deletions packages/shell-api/src/aggregation-cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
returnType,
hasAsyncChild,
ShellApiClass,
ShellResult
ShellResult,
resultSource
} from './decorators';
import {
Cursor as ServiceProviderCursor,
Expand Down Expand Up @@ -47,7 +48,8 @@ export default class AggregationCursor extends ShellApiClass {
async [asShellResult](): Promise<ShellResult> {
return {
type: 'AggregationCursor',
value: this._mongo._serviceProvider.platform === ReplPlatform.JavaShell ? this : await this._asPrintable()
value: this._mongo._serviceProvider.platform === ReplPlatform.JavaShell ? this : await this._asPrintable(),
source: this[resultSource] ?? undefined
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh cool, I've never seen '??' before.

};
}

Expand Down
56 changes: 56 additions & 0 deletions packages/shell-api/src/collection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1114,5 +1114,61 @@ describe('Collection', () => {
expect(catchedError).to.equal(expectedError);
});
});

describe('return information about the collection as metadata', async() => {
let serviceProviderCursor: StubbedInstance<ServiceProviderCursor>;

beforeEach(() => {
serviceProviderCursor = stubInterface<ServiceProviderCursor>();
serviceProviderCursor.limit.returns(serviceProviderCursor);
serviceProviderCursor.hasNext.resolves(true);
serviceProviderCursor.next.resolves({ _id: 'abc' });
});

it('works for find()', async() => {
serviceProvider.find.returns(serviceProviderCursor);
const cursor = await collection.find();
const result = await cursor[asShellResult]();
expect(result.type).to.equal('Cursor');
expect(result.value.length).to.not.equal(0);
expect(result.value[0]._id).to.equal('abc');
expect(result.source).to.deep.equal({
namespace: {
db: 'db1',
collection: 'coll1'
}
});
});

it('works for findOne()', async() => {
serviceProvider.find.returns(serviceProviderCursor);
const document = await collection.findOne({ hasBanana: true });
const result = await (document as any)[asShellResult]();
expect(result.type).to.equal('Document');
expect(result.value._id).to.equal('abc');
expect(result.source).to.deep.equal({
namespace: {
db: 'db1',
collection: 'coll1'
}
});
});

it('works for getIndexes()', async() => {
const fakeIndex = { v: 2, key: { _id: 1 }, name: '_id_' };
serviceProvider.getIndexes.resolves([fakeIndex]);

const indexResult = await collection.getIndexes();
const result = await (indexResult as any)[asShellResult]();
expect(result.type).to.equal(null);
expect(result.value).to.deep.equal([ fakeIndex ]);
expect(result.source).to.deep.equal({
namespace: {
db: 'db1',
collection: 'coll1'
}
});
});
});
});
});
14 changes: 13 additions & 1 deletion packages/shell-api/src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
ShellApiClass,
returnsPromise,
returnType,
serverVersions
serverVersions,
Namespace,
namespaceInfo,
addSourceToResults
} from './decorators';
import { ADMIN_DB, ServerVersions } from './enums';
import {
Expand Down Expand Up @@ -36,6 +39,7 @@ import PlanCache from './plan-cache';

@shellApiClassDefault
@hasAsyncChild
@addSourceToResults
export default class Collection extends ShellApiClass {
_mongo: Mongo;
_database: any; // to avoid circular ref
Expand Down Expand Up @@ -64,6 +68,10 @@ export default class Collection extends ShellApiClass {
return proxy;
}

[namespaceInfo](): Namespace {
return { db: this._database.getName(), collection: this._name };
}

/**
* Internal method to determine what is printed for this class.
*/
Expand Down Expand Up @@ -408,6 +416,7 @@ export default class Collection extends ShellApiClass {
* @returns {Cursor} The promise of the cursor.
*/
@returnsPromise
@returnType('Document')
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice stuff!

async findOne(query = {}, projection?): Promise<Document> {
const options: any = {};
if (projection) {
Expand Down Expand Up @@ -467,6 +476,7 @@ export default class Collection extends ShellApiClass {
* @returns {Document} The promise of the result.
*/
@returnsPromise
@returnType('Document')
@serverVersions(['3.2.0', ServerVersions.latest])
async findOneAndDelete(filter, options = {}): Promise<Document> {
assertArgsDefined(filter);
Expand Down Expand Up @@ -496,6 +506,7 @@ export default class Collection extends ShellApiClass {
* @returns {Document} The promise of the result.
*/
@returnsPromise
@returnType('Document')
@serverVersions(['3.2.0', ServerVersions.latest])
async findOneAndReplace(filter, replacement, options = {}): Promise<any> {
assertArgsDefined(filter);
Expand Down Expand Up @@ -531,6 +542,7 @@ export default class Collection extends ShellApiClass {
* @returns {Document} The promise of the result.
*/
@returnsPromise
@returnType('Document')
@serverVersions(['3.2.0', ServerVersions.latest])
async findOneAndUpdate(filter, update, options = {}): Promise<any> {
assertArgsDefined(filter);
Expand Down
6 changes: 4 additions & 2 deletions packages/shell-api/src/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
serverVersions,
ShellApiClass,
shellApiClassDefault,
ShellResult
ShellResult,
resultSource
} from './decorators';
import { asShellResult, ServerVersions } from './enums';
import {
Expand All @@ -33,7 +34,8 @@ export default class Cursor extends ShellApiClass {
async [asShellResult](): Promise<ShellResult> {
return {
type: 'Cursor',
value: this._mongo._serviceProvider.platform === ReplPlatform.JavaShell ? this : await this._asPrintable()
value: this._mongo._serviceProvider.platform === ReplPlatform.JavaShell ? this : await this._asPrintable(),
source: this[resultSource] ?? undefined
};
}

Expand Down
75 changes: 74 additions & 1 deletion packages/shell-api/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import {
asShellResult
} from './enums';
import { MongoshInternalError } from '@mongosh/errors';
import { addHiddenDataProperty } from './helpers';

const addSourceToResultsSymbol = Symbol('@@mongosh.addSourceToResults');
// The custom [asShellResult]() methods in Cursor and AggregationCursor require
// this, but ideally, this symbol would be local to this file.
export const resultSource = Symbol('@@mongosh.resultSource');
export const namespaceInfo = Symbol('@@mongosh.namespaceInfo');

export interface ShellApiInterface {
[asShellResult]: Function;
Expand All @@ -19,9 +26,19 @@ export interface ShellApiInterface {
[key: string]: any;
}

export interface Namespace {
db: string;
collection: string;
}

export interface ShellResultSourceInformation {
namespace: Namespace;
}

export interface ShellResult {
value: any;
type: string;
source?: ShellResultSourceInformation;
}

export class ShellApiClass implements ShellApiInterface {
Expand All @@ -34,6 +51,53 @@ export class ShellApiClass implements ShellApiInterface {
}
}

// For classes like Collection, it can be useful to attach information to the
// result about the original data source, so that downstream consumers of the
// shell can e.g. figure out how to edit a document returned from the shell.
// To that end, we wrap the methods of a class, and report back how the
// result was generated.
// We also attach the `shellApiType` and `asShellResult` properties to the
// return type (if that is possible and they are not already present), so that
// we can also provide sensible information for methods that do not return
// shell classes, like db.coll.findOne() which returns a Document (i.e. a plain
// JavaScript object).
function wrapWithAddSourceToResult(fn: Function): Function {
function addSource<T extends {}>(result: T, obj: any): T {
if (typeof result === 'object' && result !== null) {
const resultSourceInformation: ShellResultSourceInformation = {
namespace: obj[namespaceInfo](),
};
addHiddenDataProperty(result, resultSource, resultSourceInformation);
if (result[shellApiType] === undefined && (fn as any).returnType) {
addHiddenDataProperty(result, shellApiType, (fn as any).returnType);
}
if (result[asShellResult] === undefined) {
addHiddenDataProperty(
result, asShellResult, async function(): Promise<ShellResult> {
return {
// Report { type: null } if the type is not available to match
// what the shell evaluator does when it encounters values
// that do not provide [asShellResult]().
type: this[shellApiType] || null,
value: this,
source: this[resultSource]
};
});
}
}
return result;
}
const wrapper = (fn as any).returnsPromise ?
async function(...args): Promise<any> {
return addSource(await fn.call(this, ...args), this);
} : function(...args): any {
return addSource(fn.call(this, ...args), this);
};
Object.setPrototypeOf(wrapper, Object.getPrototypeOf(fn));
Object.defineProperties(wrapper, Object.getOwnPropertyDescriptors(fn));
return wrapper;
}

interface TypeSignature {
type: string;
hasAsyncChild?: boolean;
Expand All @@ -51,6 +115,7 @@ if (!global[signaturesGlobalIdentifier]) {
}

const signatures: Signatures = global[signaturesGlobalIdentifier];
signatures.Document = { type: 'Document', attributes: {} };

export const toIgnore = [asShellResult, '_asPrintable', 'constructor'];
export function shellApiClassDefault(constructor: Function): void {
Expand Down Expand Up @@ -78,6 +143,10 @@ export function shellApiClassDefault(constructor: Function): void {
propertyName.startsWith('_')
) continue;

if ((constructor as any)[addSourceToResultsSymbol]) {
descriptor.value = wrapWithAddSourceToResult(descriptor.value);
}

descriptor.value.serverVersions = descriptor.value.serverVersions || ALL_SERVER_VERSIONS;
descriptor.value.topologies = descriptor.value.topologies || ALL_TOPOLOGIES;
descriptor.value.returnType = descriptor.value.returnType || { type: 'unknown', attributes: {} };
Expand Down Expand Up @@ -151,7 +220,8 @@ export function shellApiClassDefault(constructor: Function): void {
constructor.prototype[asShellResult] = async function(): Promise<ShellResult> {
return {
type: className,
value: await this._asPrintable()
value: await this._asPrintable(),
source: this[resultSource] ?? undefined
};
};
}
Expand Down Expand Up @@ -210,3 +280,6 @@ export function classPlatforms(platformsArray: any[]): Function {
constructor.prototype.platforms = platformsArray;
};
}
export function addSourceToResults(constructor: Function): void {
(constructor as any)[addSourceToResultsSymbol] = true;
}
9 changes: 9 additions & 0 deletions packages/shell-api/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,12 @@ export function tsToSeconds(x): number {
}
return x / 4294967296; // low 32 bits are ordinal #s within a second
}

export function addHiddenDataProperty(target: object, key: string|symbol, value: any): void {
Object.defineProperty(target, key, {
value,
enumerable: false,
writable: true,
configurable: true
});
}