diff --git a/packages/i18n/src/locales/en_US.ts b/packages/i18n/src/locales/en_US.ts index b3358db159..237d8f2123 100644 --- a/packages/i18n/src/locales/en_US.ts +++ b/packages/i18n/src/locales/en_US.ts @@ -206,6 +206,11 @@ const translations: Catalog = { description: 'Applies the first argument, a function, to each document visited by the cursor and collects the return values from successive application into an array.', example: 'db.collection.aggregate(pipeline, options).map(function)' }, + maxTimeMS: { + link: 'https://docs.mongodb.com/manual/reference/method/cursor.maxTimeMS', + description: 'Specifies a cumulative time limit in milliseconds for processing operations on a cursor.', + example: 'db.collection.aggregate(pipeline, options).maxTimeMS(timeLimit)' + }, next: { link: 'https://docs.mongodb.com/manual/reference/method/cursor.next', description: 'The next document in the cursor returned by the db.collection.aggregate() method. NOTE: if the cursor is tailable with awaitData then next will block until a document is returned. To check if a document is in the cursor\'s batch without waiting, use tryNext instead', @@ -235,6 +240,21 @@ const translations: Catalog = { batchSize: { description: 'Specifies the number of documents that mongosh displays at once.', example: 'db.collection.aggregate(pipeline, options).batchSize(10)' + }, + projection: { + link: '', + description: 'Sets a field projection for the query.', + example: 'db.collection.aggregate(pipeline, options).projection(field)' + }, + sort: { + link: 'https://docs.mongodb.com/manual/reference/method/cursor.sort', + description: 'Specifies the order in which the query returns matching documents. You must apply sort() to the cursor before retrieving any documents from the database.', + example: 'db.collection.aggregate(pipeline, options).sort(sortDocument)' + }, + skip: { + link: 'https://docs.mongodb.com/manual/reference/method/cursor.skip', + description: 'Call the cursor.skip() method on a cursor to control where MongoDB begins returning results. This approach may be useful in implementing paginated results.', + example: 'db.collection.aggregate(pipeline, options).skip(offsetNumber)' } } } diff --git a/packages/shell-api/src/abstract-cursor.ts b/packages/shell-api/src/abstract-cursor.ts new file mode 100644 index 0000000000..049567ef5a --- /dev/null +++ b/packages/shell-api/src/abstract-cursor.ts @@ -0,0 +1,176 @@ +import { + shellApiClassNoHelp, + hasAsyncChild, + ShellApiClass, + returnsPromise, + toShellResult, + returnType +} from './decorators'; +import type Mongo from './mongo'; +import type { + Document, + ExplainVerbosityLike, + FindCursor as ServiceProviderCursor, + AggregationCursor as ServiceProviderAggregationCursor, +} from '@mongosh/service-provider-core'; +import { asPrintable } from './enums'; +import { CursorIterationResult } from './result'; +import { iterate, validateExplainableVerbosity, markAsExplainOutput } from './helpers'; + +@shellApiClassNoHelp +@hasAsyncChild +export abstract class AbstractCursor extends ShellApiClass { + _mongo: Mongo; + abstract _cursor: ServiceProviderAggregationCursor | ServiceProviderCursor; + _currentIterationResult: CursorIterationResult | null = null; + _batchSize: number | null = null; + + constructor(mongo: Mongo) { + super(); + this._mongo = mongo; + } + + /** + * Internal method to determine what is printed for this class. + */ + async [asPrintable](): Promise { + return (await toShellResult(this._currentIterationResult ?? await this._it())).printable; + } + + async _it(): Promise { + const results = this._currentIterationResult = new CursorIterationResult(); + await iterate(results, this._cursor, this._batchSize ?? await this._mongo._batchSize()); + results.cursorHasMore = !this.isExhausted(); + return results; + } + + @returnType('this') + batchSize(size: number): this { + this._batchSize = size; + return this; + } + + @returnsPromise + async close(options: Document): Promise { + await this._cursor.close(options); + } + + @returnsPromise + async forEach(f: (doc: Document) => void): Promise { + return this._cursor.forEach(f); + } + + @returnsPromise + async hasNext(): Promise { + return this._cursor.hasNext(); + } + + @returnsPromise + async tryNext(): Promise { + return this._cursor.tryNext(); + } + + async* [Symbol.asyncIterator]() { + let doc; + while ((doc = await this.tryNext()) !== null) { + yield doc; + } + } + + isClosed(): boolean { + return this._cursor.closed; + } + + isExhausted(): boolean { + return this.isClosed() && this.objsLeftInBatch() === 0; + } + + @returnsPromise + async itcount(): Promise { + let count = 0; + while (await this.tryNext()) { + count++; + } + return count; + } + + @returnsPromise + async toArray(): Promise { + return this._cursor.toArray(); + } + + @returnType('this') + pretty(): this { + return this; + } + + @returnType('this') + map(f: (doc: Document) => Document): this { + this._cursor.map(f); + return this; + } + + @returnType('this') + maxTimeMS(value: number): this { + this._cursor.maxTimeMS(value); + return this; + } + + @returnsPromise + async next(): Promise { + return this._cursor.next(); + } + + @returnType('this') + projection(spec: Document): this { + this._cursor.project(spec); + return this; + } + + @returnType('this') + skip(value: number): this { + this._cursor.skip(value); + return this; + } + + @returnType('this') + sort(spec: Document): this { + this._cursor.sort(spec); + return this; + } + + objsLeftInBatch(): number { + return this._cursor.bufferedCount(); + } + + @returnsPromise + async explain(verbosity?: ExplainVerbosityLike): Promise { + // TODO: @maurizio we should probably move this in the Explain class? + // NOTE: the node driver always returns the full explain plan + // for Cursor and the queryPlanner explain for AggregationCursor. + if (verbosity !== undefined) { + verbosity = validateExplainableVerbosity(verbosity); + } + const fullExplain: any = await this._cursor.explain(verbosity); + + const explain: any = { + ...fullExplain + }; + + if ( + verbosity !== 'executionStats' && + verbosity !== 'allPlansExecution' && + explain.executionStats + ) { + delete explain.executionStats; + } + + if (verbosity === 'executionStats' && + explain.executionStats && + explain.executionStats.allPlansExecution) { + delete explain.executionStats.allPlansExecution; + } + + return markAsExplainOutput(explain); + } +} diff --git a/packages/shell-api/src/aggregation-cursor.spec.ts b/packages/shell-api/src/aggregation-cursor.spec.ts index 26e27a8e94..16a8009e1c 100644 --- a/packages/shell-api/src/aggregation-cursor.spec.ts +++ b/packages/shell-api/src/aggregation-cursor.spec.ts @@ -203,7 +203,7 @@ describe('AggregationCursor', () => { it('returns an ExplainOutput object', async() => { const explained = await shellApiCursor.explain(); - expect(spCursor.explain).to.have.been.calledWith('queryPlanner'); + expect(spCursor.explain).to.have.been.calledWith(); expect((await toShellResult(explained)).type).to.equal('ExplainOutput'); expect((await toShellResult(explained)).printable).to.deep.equal({ ok: 1 }); }); diff --git a/packages/shell-api/src/aggregation-cursor.ts b/packages/shell-api/src/aggregation-cursor.ts index 02d8b9acd9..2aae7e3f37 100644 --- a/packages/shell-api/src/aggregation-cursor.ts +++ b/packages/shell-api/src/aggregation-cursor.ts @@ -1,127 +1,20 @@ -import Mongo from './mongo'; +import type Mongo from './mongo'; import { shellApiClassDefault, - returnsPromise, - returnType, - hasAsyncChild, - ShellApiClass, - toShellResult + hasAsyncChild } from './decorators'; import type { - AggregationCursor as ServiceProviderAggregationCursor, - ExplainVerbosityLike, - Document + AggregationCursor as ServiceProviderAggregationCursor } from '@mongosh/service-provider-core'; -import { CursorIterationResult } from './result'; -import { asPrintable } from './enums'; -import { iterate, validateExplainableVerbosity, markAsExplainOutput } from './helpers'; +import { AbstractCursor } from './abstract-cursor'; @shellApiClassDefault @hasAsyncChild -export default class AggregationCursor extends ShellApiClass { - _mongo: Mongo; +export default class AggregationCursor extends AbstractCursor { _cursor: ServiceProviderAggregationCursor; - _currentIterationResult: CursorIterationResult | null = null; - _batchSize: number | null = null; constructor(mongo: Mongo, cursor: ServiceProviderAggregationCursor) { - super(); + super(mongo); this._cursor = cursor; - this._mongo = mongo; - } - - async _it(): Promise { - const results = this._currentIterationResult = new CursorIterationResult(); - await iterate(results, this._cursor, this._batchSize ?? await this._mongo._batchSize()); - results.cursorHasMore = !this.isExhausted(); - return results; - } - - /** - * Internal method to determine what is printed for this class. - */ - async [asPrintable](): Promise { - return (await toShellResult(this._currentIterationResult ?? await this._it())).printable; - } - - @returnsPromise - async close(options: Document): Promise { - await this._cursor.close(options); - } - - @returnsPromise - async forEach(f: (doc: Document) => void): Promise { - return this._cursor.forEach(f); - } - - @returnsPromise - async hasNext(): Promise { - return this._cursor.hasNext(); - } - - @returnsPromise - async tryNext(): Promise { - return this._cursor.tryNext(); - } - - async* [Symbol.asyncIterator]() { - let doc; - while ((doc = await this.tryNext()) !== null) { - yield doc; - } - } - - isClosed(): boolean { - return this._cursor.closed; - } - - isExhausted(): boolean { - return this.isClosed() && this.objsLeftInBatch() === 0; - } - - objsLeftInBatch(): number { - return this._cursor.bufferedCount(); - } - - @returnsPromise - async itcount(): Promise { - let count = 0; - while (await this.tryNext()) { - count++; - } - return count; - } - - @returnType('AggregationCursor') - map(f: (doc: Document) => Document): AggregationCursor { - this._cursor.map(f); - return this; - } - - @returnsPromise - async next(): Promise { - return this._cursor.next(); - } - - @returnsPromise - async toArray(): Promise { - return this._cursor.toArray(); - } - - @returnsPromise - async explain(verbosity: ExplainVerbosityLike = 'queryPlanner'): Promise { - verbosity = validateExplainableVerbosity(verbosity); - return markAsExplainOutput(await this._cursor.explain(verbosity)); - } - - @returnType('AggregationCursor') - pretty(): AggregationCursor { - return this; - } - - @returnType('AggregationCursor') - batchSize(size: number): AggregationCursor { - this._batchSize = size; - return this; } } diff --git a/packages/shell-api/src/collection.spec.ts b/packages/shell-api/src/collection.spec.ts index 48ea8e0dc2..398503f1ca 100644 --- a/packages/shell-api/src/collection.spec.ts +++ b/packages/shell-api/src/collection.spec.ts @@ -227,7 +227,7 @@ describe('Collection', () => { { explain: true } ); - expect(explainResult).to.equal(expectedExplainResult); + expect(explainResult).to.deep.equal(expectedExplainResult); expect((await toShellResult(explainResult)).type).to.equal('ExplainOutput'); expect(serviceProviderCursor.explain).to.have.been.calledOnce; }); diff --git a/packages/shell-api/src/cursor.ts b/packages/shell-api/src/cursor.ts index c28b805555..da77c9ba6a 100644 --- a/packages/shell-api/src/cursor.ts +++ b/packages/shell-api/src/cursor.ts @@ -4,59 +4,36 @@ import { returnsPromise, returnType, serverVersions, - ShellApiClass, shellApiClassDefault, - toShellResult, deprecated } from './decorators'; import { ServerVersions, - asPrintable, CURSOR_FLAGS } from './enums'; -import { +import type { FindCursor as ServiceProviderCursor, CursorFlag, Document, CollationOptions, - ExplainVerbosityLike, ReadPreferenceLike, ReadConcernLevelId, TagSet, HedgeOptions } from '@mongosh/service-provider-core'; -import { iterate, validateExplainableVerbosity, markAsExplainOutput } from './helpers'; -import Mongo from './mongo'; -import { CursorIterationResult } from './result'; +import type Mongo from './mongo'; import { printWarning } from './deprecation-warning'; +import { AbstractCursor } from './abstract-cursor'; @shellApiClassDefault @hasAsyncChild -export default class Cursor extends ShellApiClass { - _mongo: Mongo; +export default class Cursor extends AbstractCursor { _cursor: ServiceProviderCursor; - _currentIterationResult: CursorIterationResult | null = null; _tailable = false; - _batchSize: number | null = null; constructor(mongo: Mongo, cursor: ServiceProviderCursor) { - super(); + super(mongo); this._cursor = cursor; - this._mongo = mongo; - } - - /** - * Internal method to determine what is printed for this class. - */ - async [asPrintable](): Promise { - return (await toShellResult(this._currentIterationResult ?? await this._it())).printable; - } - - async _it(): Promise { - const results = this._currentIterationResult = new CursorIterationResult(); - await iterate(results, this._cursor, this._batchSize ?? await this._mongo._batchSize()); - results.cursorHasMore = !this.isExhausted(); - return results; } /** @@ -100,17 +77,12 @@ export default class Cursor extends ShellApiClass { } @returnType('Cursor') - batchSize(size: number): Cursor { - this._batchSize = size; + batchSize(size: number): this { + super.batchSize(size); this._cursor.batchSize(size); return this; } - @returnsPromise - async close(options: Document): Promise { - await this._cursor.close(options); - } - @returnType('Cursor') @serverVersions(['3.4.0', ServerVersions.latest]) collation(spec: CollationOptions): Cursor { @@ -131,42 +103,6 @@ export default class Cursor extends ShellApiClass { return this._cursor.count(); } - @returnsPromise - async explain(verbosity?: ExplainVerbosityLike): Promise { - // TODO: @maurizio we should probably move this in the Explain class? - // NOTE: the node driver always returns the full explain plan - // for Cursor and the queryPlanner explain for AggregationCursor. - if (verbosity !== undefined) { - verbosity = validateExplainableVerbosity(verbosity); - } - const fullExplain: any = await this._cursor.explain(verbosity); - - const explain: any = { - ...fullExplain - }; - - if ( - verbosity !== 'executionStats' && - verbosity !== 'allPlansExecution' && - explain.executionStats - ) { - delete explain.executionStats; - } - - if (verbosity === 'executionStats' && - explain.executionStats && - explain.executionStats.allPlansExecution) { - delete explain.executionStats.allPlansExecution; - } - - return markAsExplainOutput(explain); - } - - @returnsPromise - async forEach(f: (doc: Document) => void): Promise { - return this._cursor.forEach(f); - } - @returnsPromise async hasNext(): Promise { if (this._tailable) { @@ -176,19 +112,7 @@ export default class Cursor extends ShellApiClass { this._mongo._internalState.context.print ); } - return this._cursor.hasNext(); - } - - @returnsPromise - async tryNext(): Promise { - return this._cursor.tryNext(); - } - - async* [Symbol.asyncIterator]() { - let doc; - while ((doc = await this.tryNext()) !== null) { - yield doc; - } + return super.hasNext(); } @returnType('Cursor') @@ -197,47 +121,18 @@ export default class Cursor extends ShellApiClass { return this; } - isClosed(): boolean { - return this._cursor.closed; - } - - isExhausted(): boolean { - return this.isClosed() && this.objsLeftInBatch() === 0; - } - - @returnsPromise - async itcount(): Promise { - let count = 0; - while (await this.tryNext()) { - count++; - } - return count; - } - @returnType('Cursor') limit(value: number): Cursor { this._cursor.limit(value); return this; } - @returnType('Cursor') - map(f: (doc: Document) => Document): Cursor { - this._cursor.map(f); - return this; - } - @returnType('Cursor') max(indexBounds: Document): Cursor { this._cursor.max(indexBounds); return this; } - @returnType('Cursor') - maxTimeMS(value: number): Cursor { - this._cursor.maxTimeMS(value); - return this; - } - @returnType('Cursor') @serverVersions(['3.2.0', ServerVersions.latest]) maxAwaitTimeMS(value: number): Cursor { @@ -260,7 +155,7 @@ export default class Cursor extends ShellApiClass { this._mongo._internalState.context.print ); } - return this._cursor.next(); + return super.next(); } @returnType('Cursor') @@ -275,12 +170,6 @@ export default class Cursor extends ShellApiClass { return this; } - @returnType('Cursor') - projection(spec: Document): Cursor { - this._cursor.project(spec); - return this; - } - @returnType('Cursor') readPref(mode: ReadPreferenceLike, tagSet?: TagSet[], hedgeOptions?: HedgeOptions): Cursor { let pref: ReadPreferenceLike; @@ -311,18 +200,6 @@ export default class Cursor extends ShellApiClass { return this._cursor.count(); } - @returnType('Cursor') - skip(value: number): Cursor { - this._cursor.skip(value); - return this; - } - - @returnType('Cursor') - sort(spec: Document): Cursor { - this._cursor.sort(spec); - return this; - } - @returnType('Cursor') @serverVersions(['3.2.0', ServerVersions.latest]) tailable(opts = { awaitData: false }): Cursor { @@ -334,16 +211,6 @@ export default class Cursor extends ShellApiClass { return this; } - @returnsPromise - async toArray(): Promise { - return this._cursor.toArray(); - } - - @returnType('Cursor') - pretty(): Cursor { - return this; - } - @deprecated @serverVersions([ServerVersions.earliest, '4.0.0']) maxScan(): void { @@ -359,10 +226,6 @@ export default class Cursor extends ShellApiClass { return this; } - objsLeftInBatch(): number { - return this._cursor.bufferedCount(); - } - @returnType('Cursor') readConcern(level: ReadConcernLevelId): Cursor { this._cursor = this._cursor.withReadConcern({ level }); diff --git a/packages/shell-api/src/database.spec.ts b/packages/shell-api/src/database.spec.ts index 331d2cbe12..b710bfaa02 100644 --- a/packages/shell-api/src/database.spec.ts +++ b/packages/shell-api/src/database.spec.ts @@ -308,7 +308,7 @@ describe('Database', () => { { explain: true } ); - expect(explainResult).to.equal(expectedExplainResult); + expect(explainResult).to.deep.equal(expectedExplainResult); expect(serviceProviderCursor.explain).to.have.been.calledOnce; }); diff --git a/packages/shell-api/src/decorators.ts b/packages/shell-api/src/decorators.ts index d5917ad2e4..04ffc92193 100644 --- a/packages/shell-api/src/decorators.ts +++ b/packages/shell-api/src/decorators.ts @@ -179,8 +179,8 @@ type ClassHelp = { attr: { name: string; description: string }[]; }; -export const toIgnore = ['constructor']; -export function shellApiClassDefault(constructor: Function): void { +export const toIgnore = ['constructor', 'help']; +export function shellApiClassGeneric(constructor: Function, hasHelp: boolean): void { const className = constructor.name; const classHelpKeyPrefix = `shell-api.classes.${className}.help`; const classHelp: ClassHelp = { @@ -222,7 +222,7 @@ export function shellApiClassDefault(constructor: Function): void { type: 'function', serverVersions: method.serverVersions, topologies: method.topologies, - returnType: method.returnType, + returnType: method.returnType === 'this' ? className : method.returnType, returnsPromise: method.returnsPromise, deprecated: method.deprecated, platforms: method.platforms @@ -250,8 +250,11 @@ export function shellApiClassDefault(constructor: Function): void { }); } - const superClass = Object.getPrototypeOf(constructor.prototype); - if (superClass.constructor.name !== 'ShellApiClass' && superClass.constructor !== Array) { + let superClass = constructor.prototype; + while ((superClass = Object.getPrototypeOf(superClass)) !== null) { + if (superClass.constructor.name === 'ShellApiClass' || superClass.constructor === Array) { + break; + } const superClassHelpKeyPrefix = `shell-api.classes.${superClass.constructor.name}.help`; for (const propertyName of Object.getOwnPropertyNames(superClass)) { const descriptor = Object.getOwnPropertyDescriptor(superClass, propertyName); @@ -268,7 +271,7 @@ export function shellApiClassDefault(constructor: Function): void { type: 'function', serverVersions: method.serverVersions, topologies: method.topologies, - returnType: method.returnType, + returnType: method.returnType === 'this' ? className : method.returnType, returnsPromise: method.returnsPromise, deprecated: method.deprecated, platforms: method.platforms @@ -289,7 +292,17 @@ export function shellApiClassDefault(constructor: Function): void { constructor.prototype[asPrintable] || ShellApiClass.prototype[asPrintable]; addHiddenDataProperty(constructor.prototype, shellApiType, className); - signatures[className] = classSignature; + if (hasHelp) { + signatures[className] = classSignature; + } +} + +export function shellApiClassDefault(constructor: Function): void { + shellApiClassGeneric(constructor, true); +} + +export function shellApiClassNoHelp(constructor: Function): void { + shellApiClassGeneric(constructor, false); } function markImplicitlyAwaited Promise>(orig: T): ((...args: Parameters) => Promise) { diff --git a/packages/shell-api/src/explainable-cursor.ts b/packages/shell-api/src/explainable-cursor.ts index a6d4ca9ef2..c1826d1165 100644 --- a/packages/shell-api/src/explainable-cursor.ts +++ b/packages/shell-api/src/explainable-cursor.ts @@ -1,8 +1,8 @@ -import { shellApiClassDefault, returnType } from './decorators'; +import { shellApiClassDefault } from './decorators'; import Cursor from './cursor'; -import Mongo from './mongo'; +import type Mongo from './mongo'; import { asPrintable } from './enums'; -import type { Document, ExplainVerbosityLike } from '@mongosh/service-provider-core'; +import type { ExplainVerbosityLike } from '@mongosh/service-provider-core'; @shellApiClassDefault export default class ExplainableCursor extends Cursor { @@ -26,9 +26,4 @@ export default class ExplainableCursor extends Cursor { this._explained ??= await this._baseCursor.explain(this._verbosity); return this._explained; } - - @returnType('ExplainableCursor') - map(f: (doc: Document) => Document): ExplainableCursor { - return super.map(f) as ExplainableCursor; - } }