From 191c69be2f2209f521d67913f2ea898986e536c6 Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Fri, 6 Nov 2020 14:40:06 -0500 Subject: [PATCH 01/14] feat: introduce AbstractCursor and its concrete subclasses This change introduces a fundamental redesign of the cursor types in the driver. The first change is to add a new `AbstractCursor` type, which is only concerned with iterating a cursor (using `getMore`) once it has been initialized. The `_initialize` method must be implemented by subclasses. The concrete subclasses are generally builders for `find` and `aggregate` commands, each providing their own custom initialization method. NODE-2809 --- package-lock.json | 8 +- package.json | 4 +- src/change_stream.ts | 113 +- src/cmap/events.ts | 5 + src/cmap/wire_protocol/get_more.ts | 2 +- src/cmap/wire_protocol/query.ts | 9 +- src/collection.ts | 47 +- src/cursor/abstract_cursor.ts | 699 +++++++ src/cursor/aggregation_cursor.ts | 112 +- src/cursor/command_cursor.ts | 83 - src/cursor/cursor.ts | 1716 ----------------- src/cursor/find_cursor.ts | 381 ++++ src/cursor/index.ts | 3 - src/db.ts | 30 +- src/gridfs-stream/download.ts | 8 +- src/gridfs-stream/index.ts | 4 +- src/index.ts | 28 +- src/mongo_client.ts | 4 +- src/operations/aggregate.ts | 1 + src/operations/command.ts | 1 - src/operations/count.ts | 108 +- src/operations/execute_operation.ts | 11 + src/operations/find.ts | 23 +- src/operations/find_one.ts | 2 +- src/operations/indexes.ts | 39 +- src/operations/list_collections.ts | 66 +- src/sdam/topology.ts | 29 - src/sessions.ts | 6 +- src/utils.ts | 5 +- test/functional/abstract_cursor.test.js | 137 ++ test/functional/aggregation.test.js | 6 +- test/functional/apm.test.js | 2 +- test/functional/causal_consistency.test.js | 3 +- test/functional/change_stream.test.js | 62 +- test/functional/collations.test.js | 8 +- test/functional/command_write_concern.test.js | 2 +- test/functional/core/cursor.test.js | 63 +- test/functional/core/extend_cursor.test.js | 89 - test/functional/core/tailable_cursor.test.js | 19 +- test/functional/core/undefined.test.js | 24 +- test/functional/crud_api.test.js | 76 +- test/functional/cursor.test.js | 611 ++---- test/functional/cursorstream.test.js | 27 +- test/functional/find.test.js | 92 +- test/functional/generator_based.test.js | 49 - test/functional/operation_example.test.js | 65 +- .../operation_generators_example.test.js | 4 +- .../operation_promises_example.test.js | 19 +- test/functional/promote_values.test.js | 2 +- test/functional/readpreference.test.js | 2 +- test/functional/spec-runner/index.js | 8 +- test/functional/unicode.test.js | 13 +- test/unit/core/response_test.js.test.js | 4 +- test/unit/sessions/collection.test.js | 2 +- 54 files changed, 2008 insertions(+), 2928 deletions(-) create mode 100644 src/cursor/abstract_cursor.ts delete mode 100644 src/cursor/command_cursor.ts delete mode 100644 src/cursor/cursor.ts create mode 100644 src/cursor/find_cursor.ts delete mode 100644 src/cursor/index.ts create mode 100644 test/functional/abstract_cursor.test.js delete mode 100644 test/functional/core/extend_cursor.test.js delete mode 100644 test/functional/generator_based.test.js diff --git a/package-lock.json b/package-lock.json index 505e4a45b7..d5aa2bf381 100644 --- a/package-lock.json +++ b/package-lock.json @@ -534,6 +534,11 @@ "integrity": "sha512-ixpV6PSSMnIVpMNCLQ0gWguC2+pBxc0LeUCv9Ugj54opVSVFXfPNYP6sMa7UHvicYGDXAyHQSAzQC8VYEIgdFQ==", "dev": true }, + "@types/lodash": { + "version": "4.14.164", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.164.tgz", + "integrity": "sha512-fXCEmONnrtbYUc5014avwBeMdhHHO8YJCkOBflUL9EoJBSKZ1dei+VO74fA7JkTHZ1GvZack2TyIw5U+1lT8jg==" + }, "@types/minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", @@ -4305,8 +4310,7 @@ "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash._reinterpolate": { "version": "3.0.0", diff --git a/package.json b/package.json index 012b1ea0a9..241b015fe5 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,11 @@ "bson-ext": "^2.0.0" }, "dependencies": { + "@types/lodash": "^4.14.164", "bl": "^2.2.1", "bson": "^4.0.4", - "denque": "^1.4.1" + "denque": "^1.4.1", + "lodash": "^4.17.20" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", diff --git a/src/change_stream.ts b/src/change_stream.ts index bd5e3fe8a5..80edeaaf4f 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -1,7 +1,6 @@ import Denque = require('denque'); import { EventEmitter } from 'events'; import { MongoError, AnyError, isResumableError } from './error'; -import { Cursor, CursorOptions, CursorStream, CursorStreamOptions } from './cursor/cursor'; import { AggregateOperation, AggregateOptions } from './operations/aggregate'; import { relayEvents, @@ -21,6 +20,14 @@ import type { CollationOptions } from './cmap/wire_protocol/write_command'; import { MongoClient } from './mongo_client'; import { Db } from './db'; import { Collection } from './collection'; +import type { Readable } from 'stream'; +import { + AbstractCursor, + AbstractCursorOptions, + CursorStreamOptions +} from './cursor/abstract_cursor'; +import type { ClientSession } from './sessions'; +import { executeOperation, ExecutionResult } from './operations/execute_operation'; const kResumeQueue = Symbol('resumeQueue'); const kCursorStream = Symbol('cursorStream'); @@ -162,13 +169,6 @@ interface UpdateDescription { removedFields: string[]; } -/** @internal */ -export class ChangeStreamStream extends CursorStream { - constructor(cursor: ChangeStreamCursor) { - super(cursor); - } -} - /** * Creates a new Change Stream instance. Normally created using {@link Collection#watch|Collection.watch()}. * @public @@ -183,7 +183,7 @@ export class ChangeStream extends EventEmitter { closed: boolean; streamOptions?: CursorStreamOptions; [kResumeQueue]: Denque; - [kCursorStream]?: CursorStream; + [kCursorStream]?: Readable; /** @event */ static readonly CLOSE = 'close' as const; @@ -252,13 +252,13 @@ export class ChangeStream extends EventEmitter { this.on('removeListener', eventName => { if (eventName === 'change' && this.listenerCount('change') === 0 && this.cursor) { - this[kCursorStream]?.removeAllListeners(CursorStream.DATA); + this[kCursorStream]?.removeAllListeners('data'); } }); } /** @internal */ - get cursorStream(): CursorStream | undefined { + get cursorStream(): Readable | undefined { return this[kCursorStream]; } @@ -325,7 +325,7 @@ export class ChangeStream extends EventEmitter { * Return a modified Readable stream including a possible transform method. * @throws MongoError if this.cursor is undefined */ - stream(options?: CursorStreamOptions): ChangeStreamStream { + stream(options?: CursorStreamOptions): Readable { this.streamOptions = options; if (!this.cursor) { throw new MongoError('ChangeStream has no cursor, unable to stream'); @@ -335,28 +335,34 @@ export class ChangeStream extends EventEmitter { } /** @public */ -export interface ChangeStreamCursorOptions extends CursorOptions { +export interface ChangeStreamCursorOptions extends AbstractCursorOptions { startAtOperationTime?: OperationTime; resumeAfter?: ResumeToken; startAfter?: boolean; } /** @internal */ -export class ChangeStreamCursor extends Cursor { +export class ChangeStreamCursor extends AbstractCursor { _resumeToken: ResumeToken; startAtOperationTime?: OperationTime; hasReceived?: boolean; resumeAfter: ResumeToken; startAfter: ResumeToken; + options: ChangeStreamCursorOptions; + + postBatchResumeToken?: Document; + pipeline: Document[]; constructor( topology: Topology, - operation: AggregateOperation, - options: ChangeStreamCursorOptions + namespace: MongoDBNamespace, + pipeline: Document[] = [], + options: ChangeStreamCursorOptions = {} ) { - super(topology, operation, options); + super(topology, namespace, options); - options = options || {}; + this.pipeline = pipeline; + this.options = options; this._resumeToken = null; this.startAtOperationTime = options.startAtOperationTime; @@ -421,18 +427,28 @@ export class ChangeStreamCursor extends Cursor { + _initialize(session: ClientSession, callback: Callback): void { + const aggregateOperation = new AggregateOperation( + { s: { namespace: this.namespace } }, + this.pipeline, + { + ...this.cursorOptions, + ...this.options, + session + } + ); + + executeOperation(this.topology, aggregateOperation, (err, response) => { if (err || response == null) { - callback(err, response); - return; + return callback(err); } + const server = aggregateOperation.server; if ( this.startAtOperationTime == null && this.resumeAfter == null && this.startAfter == null && - maxWireVersion(this.server) >= 7 + maxWireVersion(server) >= 7 ) { this.startAtOperationTime = response.operationTime; } @@ -441,15 +457,16 @@ export class ChangeStreamCursor extends Cursor { + _getMore(batchSize: number, callback: Callback): void { + super._getMore(batchSize, (err, response) => { if (err) { - callback(err); - return; + return callback(err); } this._processBatch('nextBatch', response); @@ -466,26 +483,32 @@ export class ChangeStreamCursor extends Cursor 0) { + streamEvents(changeStream, changeStreamCursor); + } - if (self.listenerCount(ChangeStream.CHANGE) > 0) streamEvents(self, changeStreamCursor); return changeStreamCursor; } @@ -532,24 +555,24 @@ function waitForTopologyConnected( } function closeWithError(changeStream: ChangeStream, error: AnyError, callback?: Callback): void { - if (!callback) changeStream.emit(ChangeStream.ERROR, error); + if (!callback) { + changeStream.emit(ChangeStream.ERROR, error); + } + changeStream.close(() => callback && callback(error)); } function streamEvents(changeStream: ChangeStream, cursor: ChangeStreamCursor): void { const stream = changeStream[kCursorStream] || cursor.stream(); changeStream[kCursorStream] = stream; - stream.on(CursorStream.DATA, change => processNewChange(changeStream, change)); - stream.on(CursorStream.ERROR, error => processError(changeStream, error)); + stream.on('data', change => processNewChange(changeStream, change)); + stream.on('error', error => processError(changeStream, error)); } function endStream(changeStream: ChangeStream): void { const cursorStream = changeStream[kCursorStream]; if (cursorStream) { - [CursorStream.DATA, CursorStream.CLOSE, CursorStream.END, CursorStream.ERROR].forEach(event => - cursorStream.removeAllListeners(event) - ); - + ['data', 'close', 'end', 'error'].forEach(event => cursorStream.removeAllListeners(event)); cursorStream.destroy(); } @@ -604,7 +627,10 @@ function processError(changeStream: ChangeStream, error: AnyError, callback?: Ca // otherwise, raise an error and close the change stream function unresumableError(err: AnyError) { - if (!callback) changeStream.emit(ChangeStream.ERROR, err); + if (!callback) { + changeStream.emit(ChangeStream.ERROR, err); + } + changeStream.close(() => processResumeQueue(changeStream, err)); } @@ -676,6 +702,7 @@ function processResumeQueue(changeStream: ChangeStream, err?: Error) { request(new MongoError('Change Stream is not open.')); return; } + request(err, changeStream.cursor); } } diff --git a/src/cmap/events.ts b/src/cmap/events.ts index 9775af02b2..98daa5548b 100644 --- a/src/cmap/events.ts +++ b/src/cmap/events.ts @@ -4,6 +4,7 @@ import type { ConnectionPool, ConnectionPoolOptions } from './connection_pool'; import type { Connection } from './connection'; import type { Document } from '../bson'; import type { AnyError } from '../error'; +import { cloneDeep } from 'lodash'; /** * The base export class for all monitoring events published from the connection pool @@ -394,6 +395,10 @@ function extractCommand(command: WriteProtocolMessageType): Document { } function extractReply(command: WriteProtocolMessageType, reply?: Document) { + if (reply) { + reply = cloneDeep(reply); + } + if (command instanceof KillCursor) { return { ok: 1, diff --git a/src/cmap/wire_protocol/get_more.ts b/src/cmap/wire_protocol/get_more.ts index f09f03fe9e..d03ebd9a2b 100644 --- a/src/cmap/wire_protocol/get_more.ts +++ b/src/cmap/wire_protocol/get_more.ts @@ -11,7 +11,7 @@ export interface GetMoreOptions extends CommandOptions { batchSize?: number; maxTimeMS?: number; maxAwaitTimeMS?: number; - comment?: Document; + comment?: Document | string; } export function getMore( diff --git a/src/cmap/wire_protocol/query.ts b/src/cmap/wire_protocol/query.ts index 24d8110367..3afebe186b 100644 --- a/src/cmap/wire_protocol/query.ts +++ b/src/cmap/wire_protocol/query.ts @@ -192,10 +192,15 @@ function prepareLegacyFindQuery( if (cmd.maxScan) findCmd['$maxScan'] = cmd.maxScan; if (cmd.min) findCmd['$min'] = cmd.min; if (cmd.max) findCmd['$max'] = cmd.max; - if (typeof cmd.showDiskLoc !== 'undefined') findCmd['$showDiskLoc'] = cmd.showDiskLoc; + if (typeof cmd.showDiskLoc !== 'undefined') { + findCmd['$showDiskLoc'] = cmd.showDiskLoc; + } else if (typeof cmd.showRecordId !== 'undefined') { + findCmd['$showDiskLoc'] = cmd.showRecordId; + } + if (cmd.comment) findCmd['$comment'] = cmd.comment; if (cmd.maxTimeMS) findCmd['$maxTimeMS'] = cmd.maxTimeMS; - if (options.explain !== undefined) { + if (options.explain) { // nToReturn must be 0 (match all) or negative (match N and close cursor) // nToReturn > 0 will give explain results equivalent to limit(0) numberToReturn = -Math.abs(cmd.limit || 0); diff --git a/src/collection.ts b/src/collection.ts index 9fdb23ce40..d54da1b792 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -16,8 +16,8 @@ import { OrderedBulkOperation } from './bulk/ordered'; import { ChangeStream, ChangeStreamOptions } from './change_stream'; import { WriteConcern, WriteConcernOptions } from './write_concern'; import { ReadConcern, ReadConcernLike } from './read_concern'; -import { AggregationCursor, CommandCursor, Cursor } from './cursor'; -import { AggregateOperation, AggregateOptions } from './operations/aggregate'; +import { AggregationCursor } from './cursor/aggregation_cursor'; +import type { AggregateOptions } from './operations/aggregate'; import { BulkWriteOperation } from './operations/bulk_write'; import { CountDocumentsOperation, CountDocumentsOptions } from './operations/count_documents'; import { @@ -29,12 +29,12 @@ import { IndexesOperation, IndexExistsOperation, IndexInformationOperation, - ListIndexesOperation, CreateIndexesOptions, DropIndexesOptions, ListIndexesOptions, IndexSpecification, - IndexDescription + IndexDescription, + ListIndexesCursor } from './operations/indexes'; import { DistinctOperation, DistinctOptions } from './operations/distinct'; import { DropCollectionOperation, DropCollectionOptions } from './operations/drop'; @@ -42,7 +42,7 @@ import { EstimatedDocumentCountOperation, EstimatedDocumentCountOptions } from './operations/estimated_document_count'; -import { FindOperation, FindOptions } from './operations/find'; +import type { FindOptions } from './operations/find'; import { FindOneOperation } from './operations/find_one'; import { FindAndModifyOperation, @@ -86,6 +86,7 @@ import type { PkFactory } from './mongo_client'; import type { Logger, LoggerOptions } from './logger'; import type { OperationParent } from './operations/command'; import type { Sort } from './sort'; +import { FindCursor } from './cursor/find_cursor'; /** @public */ export interface Collection { @@ -632,10 +633,10 @@ export class Collection implements OperationParent { * * @param filter - The query predicate. If unspecified, then all documents in the collection will match the predicate */ - find(): Cursor; - find(filter: Document): Cursor; - find(filter: Document, options: FindOptions): Cursor; - find(filter?: Document, options?: FindOptions): Cursor { + find(): FindCursor; + find(filter: Document): FindCursor; + find(filter: Document, options: FindOptions): FindCursor; + find(filter?: Document, options?: FindOptions): FindCursor { if (arguments.length > 2) { throw new TypeError('Third parameter to `collection.find()` must be undefined'); } @@ -643,11 +644,11 @@ export class Collection implements OperationParent { throw new TypeError('`options` parameter must not be function'); } - options = resolveOptions(this, options); - return new Cursor( + return new FindCursor( getTopology(this), - new FindOperation(this, this.s.namespace, filter, options), - options + this.s.namespace, + filter, + resolveOptions(this, options) ); } @@ -866,15 +867,8 @@ export class Collection implements OperationParent { * * @param options - Optional settings for the command */ - listIndexes(options?: ListIndexesOptions): CommandCursor { - options = resolveOptions(this, options); - const cursor = new CommandCursor( - getTopology(this), - new ListIndexesOperation(this, options), - options - ); - - return cursor; + listIndexes(options?: ListIndexesOptions): ListIndexesCursor { + return new ListIndexesCursor(this, resolveOptions(this, options)); } /** @@ -1218,11 +1212,12 @@ export class Collection implements OperationParent { throw new TypeError('`options` parameter must not be function'); } - options = resolveOptions(this, options); return new AggregationCursor( + this, getTopology(this), - new AggregateOperation(this, pipeline, options), - options + this.s.namespace, + pipeline, + resolveOptions(this, options) ); } @@ -1245,7 +1240,7 @@ export class Collection implements OperationParent { pipeline = []; } - return new ChangeStream(this, pipeline, options); + return new ChangeStream(this, pipeline, resolveOptions(this, options)); } /** diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts new file mode 100644 index 0000000000..e5eab2c4fa --- /dev/null +++ b/src/cursor/abstract_cursor.ts @@ -0,0 +1,699 @@ +import { Callback, maybePromise, MongoDBNamespace } from '../utils'; +import { Long, Document, BSONSerializeOptions, pluckBSONSerializeOptions } from '../bson'; +import { ClientSession } from '../sessions'; +import { MongoError } from '../error'; +import { ReadPreference, ReadPreferenceLike } from '../read_preference'; +import type { Server } from '../sdam/server'; +import type { Topology } from '../sdam/topology'; +import { Readable, Transform } from 'stream'; +import { EventEmitter } from 'events'; +import type { ExecutionResult } from '../operations/execute_operation'; + +const kId = Symbol('id'); +const kDocuments = Symbol('documents'); +const kServer = Symbol('server'); +const kNamespace = Symbol('namespace'); +const kTopology = Symbol('topology'); +const kSession = Symbol('session'); +const kOptions = Symbol('options'); +const kTransform = Symbol('transform'); +const kClosed = Symbol('closed'); +const kKilled = Symbol('killed'); + +/** @internal */ +export const CURSOR_FLAGS = [ + 'tailable', + 'oplogReplay', + 'noCursorTimeout', + 'awaitData', + 'exhaust', + 'partial' +] as const; + +/** @public */ +export interface CursorCloseOptions { + /** Bypass calling killCursors when closing the cursor. */ + skipKillCursors?: boolean; +} + +/** @public */ +export interface CursorStreamOptions { + /** A transformation method applied to each document emitted by the stream */ + transform?(doc: Document): Document; +} + +/** @public */ +export type CursorFlag = typeof CURSOR_FLAGS[number]; + +/** @internal */ +export interface AbstractCursorOptions extends BSONSerializeOptions { + session?: ClientSession; + readPreference?: ReadPreferenceLike; + batchSize?: number; + maxTimeMS?: number; + comment?: Document | string; + tailable?: boolean; + awaitData?: boolean; + noCursorTimeout?: boolean; +} + +/** @internal */ +export type InternalAbstractCursorOptions = Omit & { + // resolved + readPreference: ReadPreference; + + // cursor flags, some are deprecated + oplogReplay?: boolean; + exhaust?: boolean; + partial?: boolean; +}; + +/** @internal */ +export abstract class AbstractCursor extends EventEmitter { + [kId]?: Long; + [kSession]?: ClientSession; + [kServer]?: Server; + [kNamespace]: MongoDBNamespace; + [kDocuments]: Document[]; + [kTopology]: Topology; + [kTransform]?: (doc: Document) => Document; + [kClosed]: boolean; + [kKilled]: boolean; + [kOptions]: InternalAbstractCursorOptions; + + /** @event */ + static readonly CLOSE = 'close' as const; + + constructor( + topology: Topology, + namespace: MongoDBNamespace, + options: AbstractCursorOptions = {} + ) { + super(); + + this[kTopology] = topology; + this[kNamespace] = namespace; + this[kDocuments] = []; // TODO: https://github.com/microsoft/TypeScript/issues/36230 + this[kClosed] = false; + this[kKilled] = false; + this[kOptions] = { + readPreference: + options.readPreference && options.readPreference instanceof ReadPreference + ? options.readPreference + : ReadPreference.primary, + ...pluckBSONSerializeOptions(options) + }; + + if (typeof options.batchSize === 'number') { + this[kOptions].batchSize = options.batchSize; + } + + if (typeof options.comment !== 'undefined') { + this[kOptions].comment = options.comment; + } + + if (typeof options.maxTimeMS === 'number') { + this[kOptions].maxTimeMS = options.maxTimeMS; + } + + if (options.session instanceof ClientSession) { + this[kSession] = options.session; + } + } + + get id(): Long | undefined { + return this[kId]; + } + + get topology(): Topology { + return this[kTopology]; + } + + get server(): Server | undefined { + return this[kServer]; + } + + get namespace(): MongoDBNamespace { + return this[kNamespace]; + } + + get readPreference(): ReadPreference { + return this[kOptions].readPreference; + } + + get session(): ClientSession | undefined { + return this[kSession]; + } + + /** @internal */ + get cursorOptions(): InternalAbstractCursorOptions { + return this[kOptions]; + } + + get closed(): boolean { + return this[kClosed]; + } + + get killed(): boolean { + return this[kKilled]; + } + + // NOTE: should we remove these? They are currently needed by a number of tests + isClosed(): boolean { + return this.closed; + } + + /** Returns current buffered documents length */ + bufferedCount(): number { + return this[kDocuments].length; + } + + /** Returns current buffered documents */ + readBufferedDocuments(number: number): Document[] { + return this[kDocuments].splice(0, number); + } + + [Symbol.asyncIterator](): AsyncIterator { + return { + next: async () => { + const value = await this.next(); + return { value, done: value === null }; + } + }; + } + + stream(options?: CursorStreamOptions): Readable { + if (options?.transform) { + const transform = options.transform; + const readable = makeCursorStream(this); + + return readable.pipe( + new Transform({ + objectMode: true, + highWaterMark: 1, + transform(chunk, _, callback) { + try { + const transformed = transform(chunk); + callback(undefined, transformed); + } catch (err) { + callback(err); + } + } + }) + ); + } + + return makeCursorStream(this); + } + + hasNext(): Promise; + hasNext(callback: Callback): void; + hasNext(callback?: Callback): Promise | void { + return maybePromise(callback, done => { + if (this[kId] === Long.ZERO) { + return done(undefined, false); + } + + if (this[kDocuments].length) { + return done(undefined, true); + } + + next(this, (err, doc) => { + if (err) return done(err); + + if (doc) { + this[kDocuments].unshift(doc); + done(undefined, true); + return; + } + + done(undefined, false); + }); + }); + } + + /** Get the next available document from the cursor, returns null if no more documents are available. */ + next(): Promise; + next(callback: Callback): void; + next(callback?: Callback): Promise | void { + return maybePromise(callback, done => { + if (this[kId] === Long.ZERO) { + return done(new MongoError('Cursor is exhausted')); + } + + next(this, done); + }); + } + + /** + * Iterates over all the documents for this cursor using the iterator, callback pattern. + * + * @param iterator - The iteration callback. + * @param callback - The end callback. + */ + forEach(iterator: (doc: Document) => boolean | void): Promise; + forEach(iterator: (doc: Document) => boolean | void, callback: Callback): void; + forEach( + iterator: (doc: Document) => boolean | void, + callback?: Callback + ): Promise | void { + if (typeof iterator !== 'function') { + throw new TypeError('Missing required parameter `iterator`'); + } + + return maybePromise(callback, done => { + const transform = this[kTransform]; + const fetchDocs = () => { + next(this, (err, doc) => { + if (err || doc == null) return done(err); + if (doc == null) return done(); + + // NOTE: no need to transform because `next` will do this automatically + let result = iterator(doc); + if (result === false) return done(); + + // these do need to be transformed since they are copying the rest of the batch + const internalDocs = this[kDocuments].splice(0, this[kDocuments].length); + if (internalDocs) { + for (let i = 0; i < internalDocs.length; ++i) { + result = iterator(transform ? transform(internalDocs[i]) : internalDocs[i]); + if (result === false) return done(); + } + } + + fetchDocs(); + }); + }; + + fetchDocs(); + }); + } + + close(): void; + close(callback: Callback): void; + close(options: CursorCloseOptions): Promise; + close(options: CursorCloseOptions, callback: Callback): void; + close(options?: CursorCloseOptions | Callback, callback?: Callback): Promise | void { + if (typeof options === 'function') (callback = options), (options = {}); + options = options || {}; + + const needsToEmitClosed = !this[kClosed]; + this[kClosed] = true; + + return maybePromise(callback, done => { + const cursorId = this[kId]; + const cursorNs = this[kNamespace]; + const server = this[kServer]; + const session = this[kSession]; + + if (cursorId == null || server == null || cursorId.isZero() || cursorNs == null) { + if (needsToEmitClosed) { + this[kId] = Long.ZERO; + this.emit(AbstractCursor.CLOSE); + } + + if (session && session.owner === this) { + return session.endSession(done); + } + + return done(); + } + + this[kKilled] = true; + server.killCursors( + cursorNs.toString(), + [cursorId], + { ...pluckBSONSerializeOptions(this[kOptions]), session }, + () => { + if (session && session.owner === this) { + return session.endSession(() => { + this.emit(AbstractCursor.CLOSE); + done(); + }); + } + + this.emit(AbstractCursor.CLOSE); + done(); + } + ); + }); + } + + /** + * Returns an array of documents. The caller is responsible for making sure that there + * is enough memory to store the results. Note that the array only contains partial + * results when this cursor had been previously accessed. In that case, + * cursor.rewind() can be used to reset the cursor. + * + * @param callback - The result callback. + */ + toArray(): Promise; + toArray(callback: Callback): void; + toArray(callback?: Callback): Promise | void { + return maybePromise(callback, done => { + const docs: Document[] = []; + const transform = this[kTransform]; + const fetchDocs = () => { + // NOTE: if we add a `nextBatch` then we should use it here + next(this, (err, doc) => { + if (err) return done(err); + if (doc == null) return done(undefined, docs); + + // NOTE: no need to transform because `next` will do this automatically + docs.push(doc); + + // these do need to be transformed since they are copying the rest of the batch + const internalDocs = transform + ? this[kDocuments].splice(0, this[kDocuments].length).map(transform) + : this[kDocuments].splice(0, this[kDocuments].length); + + if (internalDocs) { + docs.push(...internalDocs); + } + + fetchDocs(); + }); + }; + + fetchDocs(); + }); + } + + // DO THESE PROPERTIES BELONG HERE? + + /** + * Add a cursor flag to the cursor + * + * @param flag - The flag to set, must be one of following ['tailable', 'oplogReplay', 'noCursorTimeout', 'awaitData', 'partial' -. + * @param value - The flag boolean value. + */ + addCursorFlag(flag: CursorFlag, value: boolean): this { + if (!CURSOR_FLAGS.includes(flag)) { + throw new MongoError(`flag ${flag} is not one of ${CURSOR_FLAGS}`); + } + + if (typeof value !== 'boolean') { + throw new MongoError(`flag ${flag} must be a boolean value`); + } + + this[kOptions][flag] = value; + return this; + } + + /** + * Map all documents using the provided function + * + * @param transform - The mapping transformation method. + */ + map(transform: (doc: Document) => Document): this { + const oldTransform = this[kTransform]; + if (oldTransform) { + this[kTransform] = doc => { + return transform(oldTransform(doc)); + }; + } else { + this[kTransform] = transform; + } + + return this; + } + + /** + * Set the ReadPreference for the cursor. + * + * @param readPreference - The new read preference for the cursor. + */ + setReadPreference(readPreference: ReadPreferenceLike): this { + if (readPreference instanceof ReadPreference) { + this[kOptions].readPreference = readPreference; + } else if (typeof readPreference === 'string') { + this[kOptions].readPreference = ReadPreference.fromString(readPreference); + } else { + throw new TypeError('Invalid read preference: ' + readPreference); + } + + return this; + } + + /** + * Set a maxTimeMS on the cursor query, allowing for hard timeout limits on queries (Only supported on MongoDB 2.6 or higher) + * + * @param value - Number of milliseconds to wait before aborting the query. + */ + maxTimeMS(value: number): this { + if (typeof value !== 'number') { + throw new TypeError('maxTimeMS must be a number'); + } + + this[kOptions].maxTimeMS = value; + return this; + } + + /** + * Set the batch size for the cursor. + * + * @param value - The number of documents to return per batch. See {@link https://docs.mongodb.com/manual/reference/command/find/|find command documentation}. + */ + batchSize(value: number): this { + if (this[kOptions].tailable) { + throw new MongoError('Tailable cursors do not support batchSize'); + } + + if (typeof value !== 'number') { + throw new TypeError('batchSize requires an integer'); + } + + this[kOptions].batchSize = value; + return this; + } + + /* @internal */ + abstract _initialize( + session: ClientSession | undefined, + callback: Callback + ): void; + + /* @internal */ + _getMore(batchSize: number, callback: Callback): void { + const cursorId = this[kId]; + const cursorNs = this[kNamespace]; + const server = this[kServer]; + + if (cursorId == null) { + callback(new MongoError('Unable to iterate cursor with no id')); + return; + } + + if (server == null) { + callback(new MongoError('Unable to iterate cursor without selected server')); + return; + } + + server.getMore( + cursorNs.toString(), + cursorId, + { + ...this[kOptions], + session: this[kSession], + batchSize + }, + callback + ); + } +} + +function nextDocument(cursor: AbstractCursor): Document | null | undefined { + if (cursor[kDocuments] == null || !cursor[kDocuments].length) { + return null; + } + + const doc = cursor[kDocuments].shift(); + if (doc) { + const transform = cursor[kTransform]; + if (transform) { + return transform(doc); + } + + return doc; + } + + return null; +} + +function next(cursor: AbstractCursor, callback: Callback): void { + const cursorId = cursor[kId]; + if (cursor.closed) { + return callback(undefined, null); + } + + if (cursor[kDocuments] && cursor[kDocuments].length) { + callback(undefined, nextDocument(cursor)); + return; + } + + if (cursorId == null) { + // All cursors must operate within a session, one must be made implicitly if not explicitly provided + if (cursor[kSession] == null && cursor[kTopology].hasSessionSupport()) { + cursor[kSession] = cursor[kTopology].startSession({ owner: cursor, explicit: true }); + } + + cursor._initialize(cursor[kSession], (err, state) => { + if (state) { + const response = state.response; + cursor[kServer] = state.server; + cursor[kSession] = state.session; + + if (response.cursor) { + cursor[kId] = + typeof response.cursor.id === 'number' + ? Long.fromNumber(response.cursor.id) + : response.cursor.id; + + if (response.cursor.ns) { + cursor[kNamespace] = MongoDBNamespace.fromString(response.cursor.ns); + } + + cursor[kDocuments] = response.cursor.firstBatch; + } else { + // NOTE: This is for support of older servers (<3.2) which do not use commands + cursor[kId] = + typeof response.cursorId === 'number' + ? Long.fromNumber(response.cursorId) + : response.cursorId; + cursor[kDocuments] = response.documents; + } + + // When server responses return without a cursor document, we close this cursor + // and return the raw server response. This is often the case for explain commands + // for example + if (cursor[kId] == null) { + cursor[kId] = Long.ZERO; + cursor[kDocuments] = [state.response]; + } + } + + if (err || cursorIsDead(cursor)) { + return cleanupCursor(cursor, () => callback(err, nextDocument(cursor))); + } + + next(cursor, callback); + }); + + return; + } + + if (cursorIsDead(cursor)) { + return cleanupCursor(cursor, () => callback(undefined, null)); + } + + // otherwise need to call getMore + const batchSize = cursor[kOptions].batchSize || 1000; + cursor._getMore(batchSize, (err, response) => { + if (response) { + const cursorId = + typeof response.cursor.id === 'number' + ? Long.fromNumber(response.cursor.id) + : response.cursor.id; + + cursor[kDocuments] = response.cursor.nextBatch; + cursor[kId] = cursorId; + } + + if (err || cursorIsDead(cursor)) { + return cleanupCursor(cursor, () => callback(err, nextDocument(cursor))); + } + + next(cursor, callback); + }); +} + +function cursorIsDead(cursor: AbstractCursor): boolean { + const cursorId = cursor[kId]; + return !!cursorId && cursorId.isZero(); +} + +function cleanupCursor(cursor: AbstractCursor, callback: Callback): void { + if (cursor[kDocuments].length === 0) { + cursor[kClosed] = true; + cursor.emit(AbstractCursor.CLOSE); + } + + const session = cursor[kSession]; + if (session && session.owner === cursor) { + session.endSession(callback); + } else { + callback(); + } +} + +function makeCursorStream(cursor: AbstractCursor) { + const readable = new Readable({ + objectMode: true, + highWaterMark: 1 + }); + + let initialized = false; + let reading = false; + let needToClose = true; // NOTE: we must close the cursor if we never read from it, use `_construct` in future node versions + + readable._read = function () { + if (initialized === false) { + needToClose = false; + initialized = true; + } + + if (!reading) { + reading = true; + readNext(); + } + }; + + readable._destroy = function (error, cb) { + if (needToClose) { + cursor.close(err => process.nextTick(cb, err || error)); + } else { + cb(error); + } + }; + + function readNext() { + needToClose = false; + next(cursor, (err, result) => { + needToClose = err ? !cursor.closed : result !== null; + + if (err) { + // NOTE: This is questionable, but we have a test backing the behavior. It seems the + // desired behavior is that a stream ends cleanly when a user explicitly closes + // a client during iteration. Alternatively, we could do the "right" thing and + // propagate the error message by removing this special case. + if (err.message.match(/server is closed/)) { + cursor.close(); + return readable.push(null); + } + + // NOTE: This is also perhaps questionable. The rationale here is that these errors tend + // to be "operation interrupted", where a cursor has been closed but there is an + // active getMore in-flight. + if (cursor.killed) { + return readable.push(null); + } + + return readable.destroy(err); + } + + if (result === null) { + readable.push(null); + } else if (readable.destroyed) { + cursor.close(); + } else { + if (readable.push(result)) { + return readNext(); + } + + reading = false; + } + }); + } + + return readable; +} diff --git a/src/cursor/aggregation_cursor.ts b/src/cursor/aggregation_cursor.ts index 3ed734c5ca..be146eb23a 100644 --- a/src/cursor/aggregation_cursor.ts +++ b/src/cursor/aggregation_cursor.ts @@ -1,12 +1,21 @@ -import { MongoError } from '../error'; -import { Cursor, CursorOptions, CursorState } from './cursor'; -import type { AggregateOperation, AggregateOptions } from '../operations/aggregate'; +import { AggregateOperation, AggregateOptions } from '../operations/aggregate'; +import { AbstractCursor } from './abstract_cursor'; +import { executeOperation, ExecutionResult } from '../operations/execute_operation'; import type { Document } from '../bson'; import type { Sort } from '../sort'; import type { Topology } from '../sdam/topology'; +import type { Callback, MongoDBNamespace } from '../utils'; +import type { ClientSession } from '../sessions'; +import type { OperationParent } from '../operations/command'; +import type { AbstractCursorOptions } from './abstract_cursor'; +import type { ExplainVerbosityLike } from '../explain'; /** @public */ -export interface AggregationCursorOptions extends CursorOptions, AggregateOptions {} +export interface AggregationCursorOptions extends AbstractCursorOptions, AggregateOptions {} + +const kParent = Symbol('parent'); +const kPipeline = Symbol('pipeline'); +const kOptions = Symbol('options'); /** * The **AggregationCursor** class is an internal class that embodies an aggregation cursor on MongoDB @@ -15,101 +24,132 @@ export interface AggregationCursorOptions extends CursorOptions, AggregateOption * or higher stream * @public */ -export class AggregationCursor extends Cursor { +export class AggregationCursor extends AbstractCursor { + [kParent]: OperationParent; // TODO: NODE-2883 + [kPipeline]: Document[]; + [kOptions]: AggregateOptions; + /** @internal */ constructor( + parent: OperationParent, topology: Topology, - operation: AggregateOperation, - options: AggregationCursorOptions = {} + namespace: MongoDBNamespace, + pipeline: Document[] = [], + options: AggregateOptions = {} ) { - super(topology, operation, options); + super(topology, namespace, options); + + this[kParent] = parent; + this[kPipeline] = pipeline; + this[kOptions] = options; } - /** Set the batch size for the cursor. See {@link https://docs.mongodb.com/manual/reference/command/aggregate|aggregation documentation} */ - batchSize(batchSize: number): this { - if (this.s.state === CursorState.CLOSED || this.isDead()) { - throw new MongoError('Cursor is closed'); - } + get pipeline(): Document[] { + return this[kPipeline]; + } - if (typeof batchSize !== 'number') { - throw new MongoError('batchSize requires an integer'); - } + /** @internal */ + _initialize(session: ClientSession | undefined, callback: Callback): void { + const aggregateOperation = new AggregateOperation(this[kParent], this[kPipeline], { + ...this[kOptions], + ...this.cursorOptions, + session + }); + + executeOperation(this.topology, aggregateOperation, (err, response) => { + if (err || response == null) return callback(err); + + // TODO: NODE-2882 + callback(undefined, { server: aggregateOperation.server, session, response }); + }); + } - this.operation.options.batchSize = batchSize; - this.cursorBatchSize = batchSize; - return this; + /** Execute the explain for the cursor */ + explain(): Promise; + explain(callback: Callback): void; + explain(verbosity?: ExplainVerbosityLike): Promise; + explain( + verbosity?: ExplainVerbosityLike | Callback, + callback?: Callback + ): Promise | void { + if (typeof verbosity === 'function') (callback = verbosity), (verbosity = true); + if (verbosity === undefined) verbosity = true; + + return executeOperation( + this.topology, + new AggregateOperation(this[kParent], this[kPipeline], { + ...this[kOptions], // NOTE: order matters here, we may need to refine this + ...this.cursorOptions, + explain: verbosity + }), + callback + ); } /** Add a group stage to the aggregation pipeline */ group($group: Document): this { - this.operation.addToPipeline({ $group }); + this.pipeline.push({ $group }); return this; } /** Add a limit stage to the aggregation pipeline */ limit($limit: number): this { - this.operation.addToPipeline({ $limit }); + this.pipeline.push({ $limit }); return this; } /** Add a match stage to the aggregation pipeline */ match($match: Document): this { - this.operation.addToPipeline({ $match }); - return this; - } - - /** Add a maxTimeMS stage to the aggregation pipeline */ - maxTimeMS(maxTimeMS: number): this { - this.operation.options.maxTimeMS = maxTimeMS; + this.pipeline.push({ $match }); return this; } /** Add a out stage to the aggregation pipeline */ out($out: number): this { - this.operation.addToPipeline({ $out }); + this.pipeline.push({ $out }); return this; } /** Add a project stage to the aggregation pipeline */ project($project: Document): this { - this.operation.addToPipeline({ $project }); + this.pipeline.push({ $project }); return this; } /** Add a lookup stage to the aggregation pipeline */ lookup($lookup: Document): this { - this.operation.addToPipeline({ $lookup }); + this.pipeline.push({ $lookup }); return this; } /** Add a redact stage to the aggregation pipeline */ redact($redact: Document): this { - this.operation.addToPipeline({ $redact }); + this.pipeline.push({ $redact }); return this; } /** Add a skip stage to the aggregation pipeline */ skip($skip: number): this { - this.operation.addToPipeline({ $skip }); + this.pipeline.push({ $skip }); return this; } /** Add a sort stage to the aggregation pipeline */ sort($sort: Sort): this { - this.operation.addToPipeline({ $sort }); + this.pipeline.push({ $sort }); return this; } /** Add a unwind stage to the aggregation pipeline */ unwind($unwind: number): this { - this.operation.addToPipeline({ $unwind }); + this.pipeline.push({ $unwind }); return this; } // deprecated methods /** @deprecated Add a geoNear stage to the aggregation pipeline */ geoNear($geoNear: Document): this { - this.operation.addToPipeline({ $geoNear }); + this.pipeline.push({ $geoNear }); return this; } } diff --git a/src/cursor/command_cursor.ts b/src/cursor/command_cursor.ts deleted file mode 100644 index 6d04f9881b..0000000000 --- a/src/cursor/command_cursor.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ReadPreference, ReadPreferenceLike } from '../read_preference'; -import { MongoError } from '../error'; -import { Cursor, CursorOptions, CursorState } from './cursor'; -import type { Topology } from '../sdam/topology'; -import type { CommandOperation } from '../operations/command'; - -/** @public */ -export type CommandCursorOptions = CursorOptions; - -/** - * The **CommandCursor** class is an internal class that embodies a - * generalized cursor based on a MongoDB command allowing for iteration over the - * results returned. It supports one by one document iteration, conversion to an - * array or can be iterated as a Node 0.10.X or higher stream - * @public - */ -export class CommandCursor extends Cursor { - /** @internal */ - constructor(topology: Topology, operation: CommandOperation, options?: CommandCursorOptions) { - super(topology, operation, options); - } - - /** - * Set the ReadPreference for the cursor. - * - * @param readPreference - The new read preference for the cursor. - */ - setReadPreference(readPreference: ReadPreferenceLike): this { - if (this.s.state === CursorState.CLOSED || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - if (this.s.state !== CursorState.INIT) { - throw new MongoError('cannot change cursor readPreference after cursor has been accessed'); - } - - if (readPreference instanceof ReadPreference) { - this.options.readPreference = readPreference; - } else if (typeof readPreference === 'string') { - this.options.readPreference = ReadPreference.fromString(readPreference); - } else { - throw new TypeError('Invalid read preference: ' + readPreference); - } - - return this; - } - - /** - * Set the batch size for the cursor. - * - * @param value - The number of documents to return per batch. See {@link https://docs.mongodb.com/manual/reference/command/find/|find command documentation}. - * @throws MongoError if cursor is closed/dead or value is not a number - */ - batchSize(value: number): this { - if (this.s.state === CursorState.CLOSED || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - if (typeof value !== 'number') { - throw new MongoError('batchSize requires an integer'); - } - - if (this.cmd.cursor) { - this.cmd.cursor.batchSize = value; - } - - this.cursorBatchSize = value; - return this; - } - - /** - * Add a maxTimeMS stage to the aggregation pipeline - * - * @param value - The state maxTimeMS value. - */ - maxTimeMS(value: number): this { - if (this.topology.lastIsMaster().minWireVersion > 2) { - this.cmd.maxTimeMS = value; - } - - return this; - } -} diff --git a/src/cursor/cursor.ts b/src/cursor/cursor.ts deleted file mode 100644 index b79ecae769..0000000000 --- a/src/cursor/cursor.ts +++ /dev/null @@ -1,1716 +0,0 @@ -import { EventEmitter } from 'events'; -import { Readable } from 'stream'; -import { deprecate } from 'util'; -import { Long, Document, BSONSerializeOptions } from '../bson'; -import { MongoError, MongoNetworkError, AnyError } from '../error'; -import { Logger } from '../logger'; -import { executeOperation } from '../operations/execute_operation'; -import { CountOperation, CountOptions } from '../operations/count'; -import { ReadPreference, ReadPreferenceLike } from '../read_preference'; -import { Callback, emitDeprecatedOptionWarning, maybePromise, MongoDBNamespace } from '../utils'; -import { Sort, SortDirection, formatSort } from '../sort'; -import { PromiseProvider } from '../promise_provider'; -import type { OperationTime, ResumeToken } from '../change_stream'; -import type { CloseOptions } from '../cmap/connection_pool'; -import type { CollationOptions } from '../cmap/wire_protocol/write_command'; -import { Aspect, Hint, OperationBase } from '../operations/operation'; -import type { Topology } from '../sdam/topology'; -import { CommandOperation, CommandOperationOptions } from '../operations/command'; -import type { ReadConcern } from '../read_concern'; -import type { Server } from '../sdam/server'; -import type { ClientSession } from '../sessions'; -import { Explain, ExplainVerbosityLike } from '../explain'; - -const kCursor = Symbol('cursor'); - -/** - * Flags allowed for cursor - * @public - */ -export const FLAGS = [ - 'tailable', - 'oplogReplay', - 'noCursorTimeout', - 'awaitData', - 'exhaust', - 'partial' -] as const; - -/** @public */ -export type CursorFlag = typeof FLAGS[number]; - -/** @public */ -export const FIELDS = ['numberOfRetries', 'tailableRetryInterval'] as const; - -/** @public */ -export interface DocumentTransforms { - /** Transform each document returned */ - doc(doc: Document): Document; - /** Transform the value returned from the initial query */ - query?(doc: Document): Document | Document[]; -} - -/** @internal */ -export interface CursorPrivate { - /** Transforms functions */ - transforms?: DocumentTransforms; - numberOfRetries: number; - tailableRetryInterval: number; - currentNumberOfRetries: number; - explicitlyIgnoreSession: boolean; - batchSize: number; - - state: CursorState; - readConcern?: ReadConcern; -} - -/** - * Possible states for a cursor - * @public - */ -export enum CursorState { - INIT = 0, - OPEN = 1, - CLOSED = 2, - GET_MORE = 3 -} - -/** @public */ -export interface CursorOptions extends CommandOperationOptions { - noCursorTimeout?: boolean; - tailable?: boolean; - awaitData?: boolean; - /** @deprecated Use `awaitData` instead */ - awaitdata?: boolean; - raw?: boolean; - hint?: Hint; - limit?: number; - skip?: number; - /** The number of documents to return per batch. See {@link https://docs.mongodb.com/manual/reference/command/find/| find command documentation} and {@link https://docs.mongodb.com/manual/reference/command/aggregate|aggregation documentation}. */ - batchSize?: number; - /** Initial documents list for cursor */ - documents?: Document[]; - /** Transform function */ - transforms?: DocumentTransforms; - cursorFactory?: typeof Cursor; - tailableRetryInterval?: number; - explicitlyIgnoreSession?: boolean; - cursor?: Document; - /** The internal topology of the created cursor */ - topology?: Topology; - /** Session to use for the operation */ - numberOfRetries?: number; - sort?: Sort; -} - -/** @public */ -export interface CursorCloseOptions { - /** Bypass calling killCursors when closing the cursor. */ - skipKillCursors?: boolean; -} - -/** @public */ -export interface CursorStreamOptions { - /** A transformation method applied to each document emitted by the stream */ - transform?(doc: Document): Document; -} - -/** @public */ -export class CursorStream extends Readable { - [kCursor]: Cursor; - options: CursorStreamOptions; - - /** @event */ - static readonly CLOSE = 'close' as const; - /** @event */ - static readonly DATA = 'data' as const; - /** @event */ - static readonly END = 'end' as const; - /** @event */ - static readonly FINISH = 'finish' as const; - /** @event */ - static readonly ERROR = 'error' as const; - /** @event */ - static readonly PAUSE = 'pause' as const; - /** @event */ - static readonly READABLE = 'readable' as const; - /** @event */ - static readonly RESUME = 'resume' as const; - - constructor(cursor: Cursor, options?: CursorStreamOptions) { - super({ objectMode: true }); - this[kCursor] = cursor; - this.options = options || {}; - } - - destroy(err?: AnyError): void { - this.pause(); - this[kCursor].close(); - super.destroy(err); - } - - /** @internal */ - _read(): void { - const cursor = this[kCursor]; - if ((cursor.s && cursor.s.state === CursorState.CLOSED) || cursor.isDead()) { - this.push(null); - return; - } - - // Get the next item - nextFunction(cursor, (err, result) => { - if (err) { - if (cursor.s && cursor.s.state === CursorState.CLOSED) return; - if (!cursor.isDead()) this.emit(CursorStream.ERROR, err); - cursor.close(() => this.emit(CursorStream.END)); - return; - } - - // If we provided a transformation method - if (typeof this.options.transform === 'function' && result != null) { - this.push(this.options.transform(result)); - return; - } - - // Return the result - this.push(result); - - if (result === null && cursor.isDead()) { - this.once(CursorStream.END, () => { - cursor.close(); - this.emit(CursorStream.FINISH); - }); - } - }); - } -} - -/** - * **CURSORS Cannot directly be instantiated** - * The `Cursor` class is an internal class that embodies a cursor on MongoDB - * allowing for iteration over the results returned from the underlying query. It supports - * one by one document iteration, conversion to an array or can be iterated as a Node 4.X - * or higher stream - * @public - * - * @example - * ```js - * // Create a projection of field a - * collection.find({}).project({a:1}) - * // Skip 1 and limit 10 - * collection.find({}).skip(1).limit(10) - * // Set batchSize on cursor to 5 - * collection.find({}).batchSize(5) - * // Set query on the cursor - * collection.find({}).filter({a:1}) - * // Add a comment to the query, allowing to correlate queries - * collection.find({}).comment('add a comment') - * // Set cursor as tailable - * collection.find({}).addCursorFlag('tailable', true) - * // Set cursor as noCursorTimeout - * collection.find({}).addCursorFlag('noCursorTimeout', true) - * // Set cursor as awaitData - * collection.find({}).addCursorFlag('awaitData', true) - * // Set cursor as partial - * collection.find({}).addCursorFlag('partial', true) - * // Set $orderby {a:1} - * collection.find({}).addQueryModifier('$orderby', {a:1}) - * // Set the cursor max - * collection.find({}).max(10) - * // Set the cursor maxTimeMS - * collection.find({}).maxTimeMS(1000) - * // Set the cursor min - * collection.find({}).min(100) - * // Set the cursor returnKey - * collection.find({}).returnKey(true) - * // Set the cursor readPreference - * collection.find({}).setReadPreference(ReadPreference.PRIMARY) - * // Set the cursor showRecordId - * collection.find({}).showRecordId(true) - * // Sets the sort order of the cursor query - * collection.find({}).sort([['a', 1]]) - * // Set the cursor hint - * collection.find({}).hint('a_1') - * ``` - * - * All options are chainable, so one can do the following. - * - * ```js - * const docs = await collection.find({}) - * .maxTimeMS(1000) - * .skip(1) - * .toArray() - * ``` - */ -export class Cursor< - O extends OperationBase = OperationBase, - T extends CursorOptions = CursorOptions -> extends EventEmitter { - /** @internal */ - operation: O; - server?: Server; - ns: string; - namespace: MongoDBNamespace; - cmd: Document; - options: T; - topology: Topology; - logger: Logger; - query?: Document; - s: CursorPrivate; - - // INTERNAL CURSOR STATE - postBatchResumeToken?: ResumeToken; - currentLimit: number; - cursorId?: Long; - lastCursorId?: Long; - cursorIndex: number; - dead: boolean; - killed: boolean; - init: boolean; - notified: boolean; - documents: Document[]; - operationTime?: OperationTime; - reconnect?: boolean; - session?: ClientSession; - streamOptions?: CursorStreamOptions; - transforms?: DocumentTransforms; - raw?: boolean; - tailable: boolean; - awaitData: boolean; - bsonOptions?: BSONSerializeOptions; - - // DEPRECATED? - _batchSize: number; - _skip: number; - _limit: number; - - /** @event */ - static readonly ERROR = 'error' as const; - - /** @event */ - static readonly CLOSE = 'close' as const; - - /** @internal */ - /** - * Create a new core `Cursor` instance. - * **NOTE** Not to be instantiated directly - * - * @param topology - The server topology instance. - * @param operation - The cursor-generating operation to run - * @param options - Optional settings for the cursor - */ - constructor(topology: Topology, operation: O, options: T = {} as T) { - super(); - - const cmd = operation.cmd ? operation.cmd : {}; - - // Set local values - this.operation = operation; - this.ns = this.operation.ns.toString(); - this.namespace = MongoDBNamespace.fromString(this.ns); - this.cmd = cmd; - this.options = this.operation.options as T; - this.topology = topology; - - const { limit, skip } = getLimitSkipBatchSizeDefaults(options, cmd); - - let cursorId = undefined; - let lastCursorId = undefined; - // Did we pass in a cursor id - if (typeof cmd === 'number') { - cursorId = Long.fromNumber(cmd); - lastCursorId = cursorId; - } else if (cmd instanceof Long) { - cursorId = cmd; - lastCursorId = cmd; - } - - // All internal state - this.cursorId = cursorId; - this.lastCursorId = lastCursorId; - this.documents = options.documents || []; - this.cursorIndex = 0; - this.dead = false; - this.killed = false; - this.init = false; - this.notified = false; - this.currentLimit = 0; - // Result field name if not a cursor (contains the array of results) - this.transforms = options.transforms; - this.raw = typeof options.raw === 'boolean' ? options.raw : cmd && 'raw' in cmd && cmd.raw; - this.tailable = typeof options.tailable === 'boolean' ? options.tailable : false; - this.awaitData = - typeof options.awaitData === 'boolean' - ? options.awaitData - : typeof options.awaitdata === 'boolean' - ? options.awaitdata - : false; - - // get rid of these? - this._limit = limit; - this._skip = skip; - - if (typeof options.session === 'object') { - this.session = options.session; - } - - // Logger - this.logger = new Logger('Cursor', topology.s.options); - - if (this.operation) { - options = this.operation.options as T; - } - - emitDeprecatedOptionWarning(options, ['promiseLibrary']); - - // Tailable cursor options - const numberOfRetries = options.numberOfRetries || 5; - const tailableRetryInterval = options.tailableRetryInterval || 500; - const currentNumberOfRetries = numberOfRetries; - - // Get the batchSize - let batchSize = 1000; - if (this.cmd.cursor && this.cmd.cursor.batchSize) { - batchSize = this.cmd.cursor.batchSize; - } else if (options.cursor && options.cursor.batchSize) { - batchSize = options.cursor.batchSize ?? 1000; - } else if (typeof options.batchSize === 'number') { - batchSize = options.batchSize; - } - - // Internal cursor state - this.s = { - // Tailable cursor options - numberOfRetries: numberOfRetries, - tailableRetryInterval: tailableRetryInterval, - currentNumberOfRetries: currentNumberOfRetries, - // State - state: CursorState.INIT, - // explicitlyIgnoreSession - explicitlyIgnoreSession: !!options.explicitlyIgnoreSession, - batchSize - }; - - // Optional ClientSession - if (!options.explicitlyIgnoreSession && options.session) { - this.session = options.session; - } - - // Translate correctly - if (this.options.noCursorTimeout === true) { - this.addCursorFlag('noCursorTimeout', true); - } - - if (this.options.sort) { - this.cmd.sort = formatSort(this.options.sort); - } - - // Set the batch size - this._batchSize = batchSize; - } - - get id(): Long | undefined { - if (this.operation) return this.cursorId; - } - - set cursorBatchSize(value: number) { - this._batchSize = value; - } - - get cursorBatchSize(): number { - return this._batchSize; - } - - set cursorLimit(value: number) { - this._limit = value; - } - - get cursorLimit(): number { - return this._limit ?? 0; - } - - set cursorSkip(value: number) { - this._skip = value; - } - - get cursorSkip(): number { - return this._skip; - } - - get readPreference(): ReadPreference { - return this.operation.readPreference; - } - - get sortValue(): Sort { - return this.cmd.sort; - } - - /** @internal */ - _initializeCursor(callback: Callback): void { - if (this.operation && this.operation.session != null) { - this.session = this.operation.session; - } else { - // implicitly create a session if one has not been provided - if (!this.s.explicitlyIgnoreSession && !this.session && this.topology.hasSessionSupport()) { - this.session = this.topology.startSession({ owner: this }); - - if (this.operation) { - this.operation.session = this.session; - } - } - } - - // NOTE: this goes away once cursors use `executeOperation` - if (this.topology.shouldCheckForSessionSupport()) { - this.topology.selectServer(ReadPreference.primaryPreferred, err => { - if (err) { - callback(err); - return; - } - - this._initializeCursor(callback); - }); - - return; - } - - const done: Callback = (err, result) => { - if (err || (this.cursorId && this.cursorId.isZero())) { - this._endSession(); - } - - if ( - this.documents.length === 0 && - this.cursorId && - this.cursorId.isZero() && - !this.tailable && - !this.awaitData - ) { - return setCursorNotified(this, callback); - } - - callback(err, result); - }; - - const queryCallback: Callback = (err, result) => { - if (err) { - return done(err); - } - - if (result.cursor) { - const document = result; - - if (result.queryFailure) { - return done(new MongoError(document)); - } - - // We have an error document, return the error - if (document.$err || document.errmsg) { - return done(new MongoError(document)); - } - - // We have a cursor document - if (document.cursor != null && typeof document.cursor !== 'string') { - const id = document.cursor.id; - // If we have a namespace change set the new namespace for getmores - if (document.cursor.ns) { - this.ns = document.cursor.ns; - } - - // Promote id to long if needed - this.cursorId = typeof id === 'number' ? Long.fromNumber(id) : id; - this.lastCursorId = this.cursorId; - this.operationTime = document.operationTime; - - // If we have a firstBatch set it - if (Array.isArray(document.cursor.firstBatch)) { - this.documents = document.cursor.firstBatch; - } - - // Return after processing command cursor - return done(undefined, result); - } - } - - // Otherwise fall back to regular find path - const cursorId = result.cursorId || 0; - this.cursorId = cursorId instanceof Long ? cursorId : Long.fromNumber(cursorId); - this.documents = result.documents || [result]; - this.lastCursorId = result.cursorId; - - // Transform the results with passed in transformation method if provided - if (this.transforms && typeof this.transforms.query === 'function') { - const transformedQuery = this.transforms.query(result); - this.documents = Array.isArray(transformedQuery) ? transformedQuery : [transformedQuery]; - } - - done(undefined, result); - }; - - if (this.logger.isDebug()) { - this.logger.debug( - `issue initial query [${JSON.stringify(this.cmd)}] with flags [${JSON.stringify( - this.query - )}]` - ); - } - - executeOperation(this.topology, this.operation as any, (err, result) => { - if (err || !result) { - done(err); - return; - } - - this.server = this.operation.server; - this.init = true; - - // set these after execution because the builder might change them before now - this.bsonOptions = this.operation.bsonOptions; - - // NOTE: this is a special internal method for cloning a cursor, consider removing - if (this.cursorId != null) { - return done(); - } - - queryCallback(err, result); - }); - } - - /** @internal */ - _endSession(): boolean; - /** @internal */ - _endSession(options: CloseOptions): boolean; - /** @internal */ - _endSession(callback: Callback): void; - _endSession(options?: CloseOptions | Callback, callback?: Callback): boolean { - if (typeof options === 'function') { - callback = options; - options = {}; - } - options = options || {}; - - const session = this.session; - - if (session && (options.force || session.owner === this)) { - this.session = undefined; - - if (this.operation) { - this.operation.clearSession(); - } - - session.endSession(callback as Callback); - return true; - } - - if (callback) { - callback(); - } - - return false; - } - - /** Checks if the cursor is dead */ - isDead(): boolean { - return this.dead === true; - } - - /** Checks if the cursor was killed by the application */ - isKilled(): boolean { - return this.killed === true; - } - - /** Checks if the cursor notified it's caller about it's death */ - isNotified(): boolean { - return this.notified === true; - } - - /** Returns current buffered documents length */ - bufferedCount(): number { - return this.documents.length - this.cursorIndex; - } - - /** Returns current buffered documents */ - readBufferedDocuments(number: number): Document[] { - const unreadDocumentsLength = this.documents.length - this.cursorIndex; - const length = number < unreadDocumentsLength ? number : unreadDocumentsLength; - let elements = this.documents.slice(this.cursorIndex, this.cursorIndex + length); - - // Transform the doc with passed in transformation method if provided - if (this.transforms && typeof this.transforms.doc === 'function') { - // Transform all the elements - for (let i = 0; i < elements.length; i++) { - elements[i] = this.transforms.doc(elements[i]); - } - } - - // Ensure we do not return any more documents than the limit imposed - // Just return the number of elements up to the limit - if (this._limit > 0 && this.currentLimit + elements.length > this._limit) { - elements = elements.slice(0, this._limit - this.currentLimit); - this.kill(); - } - - // Adjust current limit - this.currentLimit = this.currentLimit + elements.length; - this.cursorIndex = this.cursorIndex + elements.length; - - // Return elements - return elements; - } - - /** Check if there is any document still available in the cursor */ - hasNext(): Promise; - hasNext(callback: Callback): void; - hasNext(callback?: Callback): Promise | void { - if (this.s.state === CursorState.CLOSED || (this.isDead && this.isDead())) { - throw new MongoError('Cursor is closed'); - } - - return maybePromise(callback, cb => { - if (this.isNotified()) { - return cb(undefined, false); - } - - nextFunction(this, (err, doc) => { - if (err) return cb(err); - if (doc == null || this.s.state === CursorState.CLOSED || this.isDead()) { - return cb(undefined, false); - } - - this.s.state = CursorState.OPEN; - this.cursorIndex--; - cb(undefined, true); - }); - }); - } - - /** Get the next available document from the cursor, returns null if no more documents are available. */ - next(): Promise; - next(callback: Callback): void; - next(callback?: Callback): Promise | void { - return maybePromise(callback, cb => { - if (this.s.state === CursorState.CLOSED || (this.isDead && this.isDead())) { - cb(new MongoError('Cursor is closed')); - return; - } - - nextFunction(this, (err, doc) => { - if (err) return cb(err); - this.s.state = CursorState.OPEN; - cb(undefined, doc); - }); - }); - } - - /** Set the cursor query */ - filter(filter: Document): this { - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - this.cmd.query = filter; - return this; - } - - /** - * Set the cursor hint - * - * @param hint - If specified, then the query system will only consider plans using the hinted index. - */ - hint(hint: Hint): this { - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - this.cmd.hint = hint; - return this; - } - - /** - * Set the cursor min - * - * @param min - Specify a $min value to specify the inclusive lower bound for a specific index in order to constrain the results of find(). The $min specifies the lower bound for all keys of a specific index in order. - */ - min(min: number): this { - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - this.cmd.min = min; - return this; - } - - /** - * Set the cursor max - * - * @param max - Specify a $max value to specify the exclusive upper bound for a specific index in order to constrain the results of find(). The $max specifies the upper bound for all keys of a specific index in order. - */ - max(max: number): this { - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - this.cmd.max = max; - return this; - } - - /** - * Set the cursor returnKey. - * If set to true, modifies the cursor to only return the index field or fields for the results of the query, rather than documents. - * If set to true and the query does not use an index to perform the read operation, the returned documents will not contain any fields. - * - * @param value - the returnKey value. - */ - returnKey(value: boolean): this { - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - this.cmd.returnKey = value; - return this; - } - - /** - * Modifies the output of a query by adding a field $recordId to matching documents. $recordId is the internal key which uniquely identifies a document in a collection. - * - * @param value - The $showDiskLoc option has now been deprecated and replaced with the showRecordId field. $showDiskLoc will still be accepted for OP_QUERY stye find. - */ - showRecordId(value: boolean): this { - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - this.cmd.showDiskLoc = value; - return this; - } - - /** - * Set a node.js specific cursor option - * - * @param field - The cursor option to set 'numberOfRetries' | 'tailableRetryInterval'. - * @param value - The field value. - */ - setCursorOption(field: typeof FIELDS[number], value: number): this { - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - if (!FIELDS.includes(field)) { - throw new MongoError(`option ${field} is not a supported option ${FIELDS}`); - } - - Object.assign(this.s, { [field]: value }); - if (field === 'numberOfRetries') this.s.currentNumberOfRetries = value as number; - return this; - } - - /** - * Add a cursor flag to the cursor - * - * @param flag - The flag to set, must be one of following ['tailable', 'oplogReplay', 'noCursorTimeout', 'awaitData', 'partial' -. - * @param value - The flag boolean value. - */ - addCursorFlag(flag: CursorFlag, value: boolean): this { - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - if (!FLAGS.includes(flag)) { - throw new MongoError(`flag ${flag} is not a supported flag ${FLAGS}`); - } - - if (typeof value !== 'boolean') { - throw new MongoError(`flag ${flag} must be a boolean value`); - } - - if (flag === 'tailable') { - this.tailable = value; - } - - if (flag === 'awaitData') { - this.awaitData = value; - } - - this.cmd[flag] = value; - return this; - } - - /** - * Add a query modifier to the cursor query - * - * @param name - The query modifier (must start with $, such as $orderby etc) - * @param value - The modifier value. - */ - addQueryModifier(name: string, value: string | boolean | number): this { - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - if (name[0] !== '$') { - throw new MongoError(`${name} is not a valid query modifier`); - } - - // Strip of the $ - const field = name.substr(1); - // Set on the command - this.cmd[field] = value; - // Deal with the special case for sort - if (field === 'orderby') this.cmd.sort = this.cmd[field]; - return this; - } - - /** - * Add a comment to the cursor query allowing for tracking the comment in the log. - * - * @param value - The comment attached to this query. - */ - comment(value: string): this { - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - this.cmd.comment = value; - return this; - } - - /** - * Set a maxAwaitTimeMS on a tailing cursor query to allow to customize the timeout value for the option awaitData (Only supported on MongoDB 3.2 or higher, ignored otherwise) - * - * @param value - Number of milliseconds to wait before aborting the tailed query. - */ - maxAwaitTimeMS(value: number): this { - if (typeof value !== 'number') { - throw new MongoError('maxAwaitTimeMS must be a number'); - } - - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - this.cmd.maxAwaitTimeMS = value; - return this; - } - - /** - * Set a maxTimeMS on the cursor query, allowing for hard timeout limits on queries (Only supported on MongoDB 2.6 or higher) - * - * @param value - Number of milliseconds to wait before aborting the query. - */ - maxTimeMS(value: number): this { - if (typeof value !== 'number') { - throw new MongoError('maxTimeMS must be a number'); - } - - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - this.cmd.maxTimeMS = value; - return this; - } - - /** - * Sets a field projection for the query. - * - * @param value - The field projection object. - */ - project(value: Document): this { - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - this.cmd.fields = value; - return this; - } - - /** - * Sets the sort order of the cursor query. - * - * @param sort - The key or keys set for the sort. - * @param direction - The direction of the sorting (1 or -1). - */ - sort(sort: Sort | string, direction?: SortDirection): this { - if (this.options.tailable) { - throw new MongoError('Tailable cursor does not support sorting'); - } - - if (this.s.state === CursorState.CLOSED || this.s.state === CursorState.OPEN || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - this.cmd.sort = formatSort(sort, direction); - return this; - } - - /** - * Set the batch size for the cursor. - * - * @param value - The number of documents to return per batch. See {@link https://docs.mongodb.com/manual/reference/command/find/|find command documentation}. - */ - batchSize(value: number): this { - if (this.options.tailable) { - throw new MongoError('Tailable cursor does not support batchSize'); - } - - if (this.s.state === CursorState.CLOSED || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - if (typeof value !== 'number') { - throw new MongoError('batchSize requires an integer'); - } - - this.cmd.batchSize = value; - this._batchSize = value; - return this; - } - - /** - * Set the collation options for the cursor. - * - * @param value - The cursor collation options (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields). - */ - collation(value: CollationOptions): this { - this.cmd.collation = value; - return this; - } - - /** - * Set the limit for the cursor. - * - * @param value - The limit for the cursor query. - */ - limit(value: number): this { - if (this.options.tailable) { - throw new MongoError('Tailable cursor does not support limit'); - } - - if (this.s.state === CursorState.OPEN || this.s.state === CursorState.CLOSED || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - if (typeof value !== 'number') { - throw new MongoError('limit requires an integer'); - } - - this.cmd.limit = value; - this.cursorLimit = value; - return this; - } - - /** - * Set the skip for the cursor. - * - * @param value - The skip for the cursor query. - */ - skip(value: number): this { - if (this.options.tailable) { - throw new MongoError('Tailable cursor does not support skip'); - } - - if (this.s.state === CursorState.OPEN || this.s.state === CursorState.CLOSED || this.isDead()) { - throw new MongoError('Cursor is closed'); - } - - if (typeof value !== 'number') { - throw new MongoError('skip requires an integer'); - } - - if (this.cmd) { - this.cmd.skip = value; - } - this.cursorSkip = value; - return this; - } - - /** Resets local state for this cursor instance, and issues a `killCursors` command to the server */ - kill(callback?: Callback): void { - // Set cursor to dead - this.dead = true; - this.killed = true; - // Remove documents - this.documents = []; - - // If no cursor id just return - if (this.cursorId == null) { - if (callback) callback(undefined, null); - return; - } - - if (this.cursorId == null || this.cursorId.isZero() || this.init === false) { - if (callback) callback(undefined, null); - return; - } - - if (!this.server) { - if (callback) callback(new MongoError('Cursor is uninitialized.')); - return; - } - - this.server.killCursors( - this.ns, - [this.cursorId], - // TODO: need to pass session here, but its leading to session leaks - { session: this.session, ...this.bsonOptions }, - callback - ); - } - - /** Resets the cursor */ - rewind(): void { - if (this.init) { - if (!this.dead) { - this.kill(); - } - - this.currentLimit = 0; - this.init = false; - this.dead = false; - this.killed = false; - this.notified = false; - this.documents = []; - this.cursorId = undefined; - this.cursorIndex = 0; - } - } - - /** Clone the cursor */ - clone(): this { - return new (this.constructor as any)(this.topology, this.operation, this.options); - } - - /** - * Iterates over all the documents for this cursor. As with `cursor.toArray`, - * not all of the elements will be iterated if this cursor had been previously accessed. - * In that case, `cursor.rewind` can be used to reset the cursor. However, unlike - * `cursor.toArray`, the cursor will only hold a maximum of batch size elements - * at any given time if batch size is specified. Otherwise, the caller is responsible - * for making sure that the entire result can fit the memory. - * - * @deprecated Please use {@link Cursor.forEach} instead - */ - each(callback: EachCallback): void { - // Rewind cursor state - this.rewind(); - // Set current cursor to INIT - this.s.state = CursorState.INIT; - // Run the query - each(this, callback); - } - - /** - * Iterates over all the documents for this cursor using the iterator, callback pattern. - * - * @param iterator - The iteration callback. - * @param callback - The end callback. - */ - forEach(iterator: (doc: Document) => void): Promise; - forEach(iterator: (doc: Document) => void, callback: Callback): void; - forEach(iterator: (doc: Document) => void, callback?: Callback): Promise | void { - if (typeof iterator !== 'function') { - throw new TypeError('Missing required parameter `iterator`'); - } - - // Rewind cursor state - this.rewind(); - - // Set current cursor to INIT - this.s.state = CursorState.INIT; - - return maybePromise(callback, done => { - each(this, (err, doc) => { - if (err) return done(err); - if (doc != null) return iterator(doc); - done(); - }); - }); - } - - /** - * Set the ReadPreference for the cursor. - * - * @param readPreference - The new read preference for the cursor. - */ - setReadPreference(readPreference: ReadPreferenceLike): this { - if (this.s.state !== CursorState.INIT) { - throw new MongoError('cannot change cursor readPreference after cursor has been accessed'); - } - - if (readPreference instanceof ReadPreference) { - this.options.readPreference = readPreference; - } else if (typeof readPreference === 'string') { - this.options.readPreference = ReadPreference.fromString(readPreference); - } else { - throw new TypeError('Invalid read preference: ' + readPreference); - } - - return this; - } - - /** - * Returns an array of documents. The caller is responsible for making sure that there - * is enough memory to store the results. Note that the array only contains partial - * results when this cursor had been previously accessed. In that case, - * cursor.rewind() can be used to reset the cursor. - * - * @param callback - The result callback. - */ - toArray(): Promise; - toArray(callback: Callback): void; - toArray(callback?: Callback): Promise | void { - if (this.options.tailable) { - throw new MongoError('Tailable cursor cannot be converted to array'); - } - - return maybePromise(callback, cb => { - const items: Document[] = []; - // Reset cursor - this.rewind(); - this.s.state = CursorState.INIT; - - // Fetch all the documents - const fetchDocs = () => { - nextFunction(this, (err, doc) => { - if (err) { - return cb(err); - } - - if (doc == null) { - return this.close({ skipKillCursors: true }, () => cb(undefined, items)); - } - - // Add doc to items - items.push(doc); - - // Get all buffered objects - if (this.bufferedCount() > 0) { - const docs = this.readBufferedDocuments(this.bufferedCount()); - items.push(...docs); - } - - // Attempt a fetch - fetchDocs(); - }); - }; - - fetchDocs(); - }); - } - - /** - * Get the count of documents for this cursor - * - * @param applySkipLimit - Should the count command apply limit and skip settings on the cursor or in the passed in options. - */ - - count(): Promise; - count(callback: Callback): void; - count(applySkipLimit: boolean): Promise; - count(applySkipLimit: boolean, callback: Callback): void; - count(applySkipLimit: boolean, options: CountOptions): Promise; - count(applySkipLimit: boolean, options: CountOptions, callback: Callback): void; - count( - applySkipLimit?: boolean | CountOptions | Callback, - options?: CountOptions | Callback, - callback?: Callback - ): Promise | void { - if (this.cmd.query == null) { - throw new MongoError('count can only be used with find command'); - } - - if (typeof options === 'function') (callback = options), (options = {}); - options = options || {}; - - if (typeof applySkipLimit === 'function') { - callback = applySkipLimit; - applySkipLimit = true; - } - - if (this.session) { - options = Object.assign({}, options, { session: this.session }); - } - - const countOperation = new CountOperation(this, !!applySkipLimit, options); - return executeOperation(this.topology, countOperation, callback); - } - - /** Close the cursor, sending a KillCursor command and emitting close. */ - close(): Promise; - close(callback: Callback): void; - close(options: CursorCloseOptions): Promise; - close(options: CursorCloseOptions, callback: Callback): void; - close( - optionsOrCallback?: CursorCloseOptions | Callback, - callback?: Callback - ): Promise | void { - const options = - typeof optionsOrCallback === 'function' - ? { skipKillCursors: false } - : Object.assign({}, optionsOrCallback); - callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : callback; - - return maybePromise(callback, cb => { - this.s.state = CursorState.CLOSED; - - if (!options.skipKillCursors) { - // Kill the cursor - this.kill(() => { - this._endSession(() => { - this.emit(Cursor.CLOSE); - cb(undefined, this); - }); - }); - - return; - } - - this._endSession(() => { - this.emit(Cursor.CLOSE); - cb(undefined, this); - }); - }); - } - - /** - * Map all documents using the provided function - * - * @param transform - The mapping transformation method. - */ - map(transform: DocumentTransforms['doc']): this { - if (this.transforms && this.transforms.doc) { - const oldTransform = this.transforms.doc; - this.transforms.doc = doc => { - return transform(oldTransform(doc)); - }; - } else { - this.transforms = { doc: transform }; - } - - return this; - } - - isClosed(): boolean { - return this.isDead(); - } - - /** Return a modified Readable stream including a possible transform method. */ - stream(options?: CursorStreamOptions): CursorStream { - return new CursorStream(this, options); - } - - /** - * Execute the explain for the cursor - * - * @param verbosity - The mode in which to run the explain. - * @param callback - The result callback. - */ - explain(verbosity?: ExplainVerbosityLike): Promise; - explain(verbosity?: ExplainVerbosityLike, callback?: Callback): Promise | void { - if (typeof verbosity === 'function') (callback = verbosity), (verbosity = true); - if (verbosity === undefined) verbosity = true; - - // TODO: For now, we need to manually do these checks. This will change after cursor refactor. - if ( - !(this.operation instanceof CommandOperation) || - !this.operation.hasAspect(Aspect.EXPLAINABLE) - ) { - throw new MongoError('This command cannot be explained'); - } - this.operation.explain = new Explain(verbosity); - - return maybePromise(callback, cb => nextFunction(this, cb)); - } - - /** Return the cursor logger */ - getLogger(): Logger { - return this.logger; - } - - [Symbol.asyncIterator](): AsyncIterator { - const Promise = PromiseProvider.get(); - return { - next: () => { - if (this.isClosed()) { - return Promise.resolve({ value: null, done: true }); - } - return this.next().then(value => ({ value, done: value === null })); - } - }; - } - // Internal methods - - /** @internal */ - _getMore(callback: Callback): void { - if (this.logger.isDebug()) { - this.logger.debug(`schedule getMore call for query [${JSON.stringify(this.query)}]`); - } - - if (this.cursorId == null) { - if (callback) callback(new MongoError('getMore attempted on invalid cursor id')); - return; - } - - // Set the current batchSize - let batchSize = this._batchSize; - if (this._limit > 0 && this.currentLimit + batchSize > this._limit) { - batchSize = this._limit - this.currentLimit; - } - - if (!this.server) { - return callback(new MongoError('Cursor is uninitialized.')); - } - - this.server.getMore( - this.ns, - this.cursorId, - { batchSize, session: this.session, ...this.bsonOptions }, - (err, response) => { - if (response) { - const cursorId = - typeof response.cursor.id === 'number' - ? Long.fromNumber(response.cursor.id) - : response.cursor.id; - - this.documents = response.cursor.nextBatch; - this.cursorId = cursorId; - } - - if (err || (this.cursorId && this.cursorId.isZero())) { - this._endSession(() => callback(err, response)); - return; - } - - callback(err, response); - } - ); - } -} - -/** Validate if the cursor is dead but was not explicitly killed by user */ -function isCursorDeadButNotkilled(self: Cursor, callback: Callback) { - // Cursor is dead but not marked killed, return null - if (self.dead && !self.killed) { - self.killed = true; - setCursorNotified(self, callback); - return true; - } - - return false; -} - -/** Validate if the cursor is dead and was killed by user */ -function isCursorDeadAndKilled(self: Cursor, callback: Callback) { - if (self.dead && self.killed) { - callback(new MongoError('cursor is dead')); - return true; - } - - return false; -} - -/** Validate if the cursor was killed by the user */ -function isCursorKilled(self: Cursor, callback: Callback) { - if (self.killed) { - setCursorNotified(self, callback); - return true; - } - - return false; -} - -/** Mark cursor as being dead and notified */ -function setCursorDeadAndNotified(self: Cursor, callback: Callback) { - self.dead = true; - setCursorNotified(self, callback); -} - -/** Mark cursor as being notified */ -function setCursorNotified(self: Cursor, callback: Callback) { - _setCursorNotifiedImpl(self, () => callback(undefined, null)); -} - -/** @internal */ -function _setCursorNotifiedImpl(self: Cursor, callback: Callback) { - self.notified = true; - self.documents = []; - self.cursorIndex = 0; - - if (self.session) { - self._endSession(callback); - return; - } - - return callback(); -} - -/** @internal */ -function nextFunction(self: Cursor, callback: Callback) { - // We have notified about it - if (self.notified) { - return callback(new Error('cursor is exhausted')); - } - - // Cursor is killed return null - if (isCursorKilled(self, callback)) return; - - // Cursor is dead but not marked killed, return null - if (isCursorDeadButNotkilled(self, callback)) return; - - // We have a dead and killed cursor, attempting to call next should error - if (isCursorDeadAndKilled(self, callback)) return; - - // We have just started the cursor - if (!self.init) { - // Topology is not connected, save the call in the provided store to be - // Executed at some point when the handler deems it's reconnected - if (!self.topology.isConnected()) { - // Only need this for single server, because repl sets and mongos - // will always continue trying to reconnect - if (self.topology._type === 'server' && !self.topology.s.options.reconnect) { - // Reconnect is disabled, so we'll never reconnect - return callback(new MongoError('no connection available')); - } - } - - self._initializeCursor((err, result) => { - if (err || result === null) { - callback(err, result); - return; - } - - nextFunction(self, callback); - }); - - return; - } - - const cursorId = self.cursorId; - if (!cursorId) { - return callback(new MongoError('Undefined cursor ID')); - } - - if (self._limit > 0 && self.currentLimit >= self._limit) { - // Ensure we kill the cursor on the server - return self.kill(() => - // Set cursor in dead and notified state - setCursorDeadAndNotified(self, callback) - ); - } else if (self.cursorIndex === self.documents.length && !Long.ZERO.equals(cursorId)) { - // Ensure an empty cursor state - self.documents = []; - self.cursorIndex = 0; - - // Check if topology is destroyed - if (self.topology.isDestroyed()) { - return callback( - new MongoNetworkError('connection destroyed, not possible to instantiate cursor') - ); - } - - // Execute the next get more - self._getMore(err => { - if (err) { - return callback(err); - } - - // Tailable cursor getMore result, notify owner about it - // No attempt is made here to retry, this is left to the user of the - // core module to handle to keep core simple - if (self.documents.length === 0 && self.tailable && Long.ZERO.equals(cursorId)) { - // No more documents in the tailed cursor - return callback(new MongoError('No more documents in tailed cursor')); - } else if (self.documents.length === 0 && self.tailable && !Long.ZERO.equals(cursorId)) { - return nextFunction(self, callback); - } - - if (self._limit > 0 && self.currentLimit >= self._limit) { - return setCursorDeadAndNotified(self, callback); - } - - nextFunction(self, callback); - }); - } else if ( - self.documents.length === self.cursorIndex && - self.tailable && - Long.ZERO.equals(cursorId) - ) { - return callback(new MongoError('No more documents in tailed cursor')); - } else if (self.documents.length === self.cursorIndex && Long.ZERO.equals(cursorId)) { - setCursorDeadAndNotified(self, callback); - } else { - if (self._limit > 0 && self.currentLimit >= self._limit) { - // Ensure we kill the cursor on the server - self.kill(() => - // Set cursor in dead and notified state - setCursorDeadAndNotified(self, callback) - ); - - return; - } - - // Increment the current cursor limit - self.currentLimit += 1; - - // Get the document - let doc = self.documents[self.cursorIndex++]; - - // Doc overflow - if (!doc || doc.$err) { - // Ensure we kill the cursor on the server - self.kill(() => - // Set cursor in dead and notified state - setCursorDeadAndNotified(self, () => callback(new MongoError(doc ? doc.$err : undefined))) - ); - - return; - } - - // Transform the doc with passed in transformation method if provided - if (self.transforms && typeof self.transforms.doc === 'function') { - doc = self.transforms.doc(doc); - } - - // Return the document - callback(undefined, doc); - } -} - -/** @internal */ -function getLimitSkipBatchSizeDefaults(options: CursorOptions, cmd: Document) { - cmd = cmd ? cmd : {}; - let limit = options.limit; - - if (!limit) { - if ('limit' in cmd) { - limit = cmd.limit; - } - if (!limit) { - limit = 0; - } - } - let skip = options.skip; - if (!skip) { - if ('skip' in cmd) { - skip = cmd.skip; - } - if (!skip) { - skip = 0; - } - } - let batchSize = options.batchSize; - if (!batchSize) { - if ('batchSize' in cmd) { - batchSize = cmd.batchSize; - } - if (!batchSize) { - batchSize = 1000; - } - } - - return { limit, skip, batchSize }; -} - -/** @public */ -export type EachCallback = (error?: AnyError, result?: Document | null) => boolean | void; - -/** - * Iterates over all the documents for this cursor. See Cursor.prototype.each for more information. - * @internal - * - * @deprecated Please use forEach instead - * @param cursor - The Cursor instance on which to run. - * @param callback - The result callback. - */ -export function each(cursor: Cursor, callback: EachCallback): void { - if (!callback) throw new MongoError('callback is mandatory'); - if (cursor.isNotified()) return; - if (cursor.s.state === CursorState.CLOSED || cursor.isDead()) { - callback(new MongoError('Cursor is closed')); - return; - } - - if (cursor.s.state === CursorState.INIT) { - cursor.s.state = CursorState.OPEN; - } - - // Define function to avoid global scope escape - let fn = null; - // Trampoline all the entries - if (cursor.bufferedCount() > 0) { - while ((fn = loop(cursor, callback))) fn(cursor, callback); - each(cursor, callback); - } else { - cursor.next((err, item) => { - if (err) return callback(err); - if (item == null) { - return cursor.close({ skipKillCursors: true }, () => callback(undefined, null)); - } - - if (callback(undefined, item) === false) return; - each(cursor, callback); - }); - } -} - -/** - * Trampoline emptying the number of retrieved items without incurring a nextTick operation - * @internal - */ -function loop(cursor: Cursor, callback: Callback) { - // No more items we are done - if (cursor.bufferedCount() === 0) return; - // Get the next document - nextFunction(cursor, callback); - // Loop - return loop; -} - -/** - * Returns an array of documents. See Cursor.prototype.toArray for more information. - * @internal - * - * @param cursor - The Cursor instance from which to get the next document. - */ -export function toArray(cursor: Cursor, callback: Callback): void { - const items: Document[] = []; - - // Reset cursor - cursor.rewind(); - cursor.s.state = CursorState.INIT; - - // Fetch all the documents - const fetchDocs = () => { - nextFunction(cursor, (err, doc) => { - if (err) { - return callback(err); - } - - if (doc == null) { - return cursor.close({ skipKillCursors: true }, () => callback(undefined, items)); - } - - // Add doc to items - items.push(doc); - - // Get all buffered objects - if (cursor.bufferedCount() > 0) { - let docs = cursor.readBufferedDocuments(cursor.bufferedCount()); - - // Transform the doc if transform method added - if (cursor.s.transforms && typeof cursor.s.transforms.doc === 'function') { - docs = docs.map(cursor.s.transforms.doc); - } - - items.push(...docs); - } - - // Attempt a fetch - fetchDocs(); - }); - }; - - fetchDocs(); -} - -// deprecated methods -deprecate(Cursor.prototype.each, 'Cursor.each is deprecated. Use Cursor.forEach instead.'); diff --git a/src/cursor/find_cursor.ts b/src/cursor/find_cursor.ts new file mode 100644 index 0000000000..5e64658b51 --- /dev/null +++ b/src/cursor/find_cursor.ts @@ -0,0 +1,381 @@ +import type { Document } from '../bson'; +import type { CollationOptions } from '../cmap/wire_protocol/write_command'; +import { MongoError } from '../error'; +import type { ExplainVerbosityLike } from '../explain'; +import { CountOperation, CountOptions } from '../operations/count'; +import { executeOperation, ExecutionResult } from '../operations/execute_operation'; +import { FindOperation, FindOptions } from '../operations/find'; +import type { Hint } from '../operations/operation'; +import type { Topology } from '../sdam/topology'; +import type { ClientSession } from '../sessions'; +import { formatSort, Sort, SortDirection } from '../sort'; +import type { Callback, MongoDBNamespace } from '../utils'; +import { AbstractCursor } from './abstract_cursor'; + +/** @internal */ +const kFilter = Symbol('filter'); +const kNumReturned = Symbol('numReturned'); +const kBuiltOptions = Symbol('builtOptions'); + +/** @public Flags allowed for cursor */ +export const FLAGS = [ + 'tailable', + 'oplogReplay', + 'noCursorTimeout', + 'awaitData', + 'exhaust', + 'partial' +] as const; + +/** @public */ +export class FindCursor extends AbstractCursor { + [kFilter]: Document; + [kNumReturned]?: number; + [kBuiltOptions]: FindOptions; + + constructor( + topology: Topology, + namespace: MongoDBNamespace, + filter: Document | undefined, + options: FindOptions = {} + ) { + super(topology, namespace, options); + + this[kFilter] = filter || {}; + this[kBuiltOptions] = options; + + if (typeof options.sort !== 'undefined') { + this[kBuiltOptions].sort = formatSort(options.sort); + } + } + + /** @internal */ + _initialize(session: ClientSession | undefined, callback: Callback): void { + this[kBuiltOptions] = Object.freeze(this[kBuiltOptions]); + const findOperation = new FindOperation(undefined, this.namespace, this[kFilter], { + ...this[kBuiltOptions], // NOTE: order matters here, we may need to refine this + ...this.cursorOptions, + session + }); + + executeOperation(this.topology, findOperation, (err, response) => { + if (err || response == null) return callback(err); + + // TODO: We only need this for legacy queries that do not support `limit`, maybe + // the value should only be saved in those cases. + if (response.cursor) { + this[kNumReturned] = response.cursor.firstBatch.length; + } else { + this[kNumReturned] = response.documents ? response.documents.length : 0; + } + + // TODO: NODE-2882 + callback(undefined, { server: findOperation.server, session, response }); + }); + } + + /** @internal */ + _getMore(batchSize: number, callback: Callback): void { + // NOTE: this is to support client provided limits in pre-command servers + const numReturned = this[kNumReturned]; + if (numReturned) { + const limit = this[kBuiltOptions].limit; + batchSize = + limit && limit > 0 && numReturned + batchSize > limit ? limit - numReturned : batchSize; + + if (batchSize <= 0) { + return this.close(callback); + } + } + + super._getMore(batchSize, (err, response) => { + if (err) return callback(err); + + // TODO: wrap this in some logic to prevent it from happening if we don't need this support + if (response) { + this[kNumReturned] = this[kNumReturned] + response.cursor.nextBatch.length; + } + + callback(undefined, response); + }); + } + + /** Get the count of documents for this cursor */ + count(): Promise; + count(callback: Callback): void; + count(options: CountOptions): Promise; + count(options: CountOptions, callback: Callback): void; + count( + options?: CountOptions | Callback, + callback?: Callback + ): Promise | void { + if (typeof options === 'boolean') { + throw new TypeError('Invalid first parameter to count'); + } + + if (typeof options === 'function') (callback = options), (options = {}); + options = options || {}; + + return executeOperation( + this.topology, + new CountOperation(this.namespace, this[kFilter], { + ...this[kBuiltOptions], // NOTE: order matters here, we may need to refine this + ...this.cursorOptions, + ...options + }), + callback + ); + } + + /** Execute the explain for the cursor */ + explain(): Promise; + explain(callback: Callback): void; + explain(verbosity?: ExplainVerbosityLike): Promise; + explain( + verbosity?: ExplainVerbosityLike | Callback, + callback?: Callback + ): Promise | void { + if (typeof verbosity === 'function') (callback = verbosity), (verbosity = true); + if (verbosity === undefined) verbosity = true; + + return executeOperation( + this.topology, + new FindOperation(undefined, this.namespace, this[kFilter], { + ...this[kBuiltOptions], // NOTE: order matters here, we may need to refine this + ...this.cursorOptions, + explain: verbosity + }), + callback + ); + } + + /** Set the cursor query */ + filter(filter: Document): this { + this[kFilter] = filter; + return this; + } + + /** + * Set the cursor hint + * + * @param hint - If specified, then the query system will only consider plans using the hinted index. + */ + hint(hint: Hint): this { + this[kBuiltOptions].hint = hint; + return this; + } + + /** + * Set the cursor min + * + * @param min - Specify a $min value to specify the inclusive lower bound for a specific index in order to constrain the results of find(). The $min specifies the lower bound for all keys of a specific index in order. + */ + min(min: number): this { + this[kBuiltOptions].min = min; + return this; + } + + /** + * Set the cursor max + * + * @param max - Specify a $max value to specify the exclusive upper bound for a specific index in order to constrain the results of find(). The $max specifies the upper bound for all keys of a specific index in order. + */ + max(max: number): this { + this[kBuiltOptions].max = max; + return this; + } + + /** + * Set the cursor returnKey. + * If set to true, modifies the cursor to only return the index field or fields for the results of the query, rather than documents. + * If set to true and the query does not use an index to perform the read operation, the returned documents will not contain any fields. + * + * @param value - the returnKey value. + */ + returnKey(value: boolean): this { + this[kBuiltOptions].returnKey = value; + return this; + } + + /** + * Modifies the output of a query by adding a field $recordId to matching documents. $recordId is the internal key which uniquely identifies a document in a collection. + * + * @param value - The $showDiskLoc option has now been deprecated and replaced with the showRecordId field. $showDiskLoc will still be accepted for OP_QUERY stye find. + */ + showRecordId(value: boolean): this { + this[kBuiltOptions].showRecordId = value; + return this; + } + + /** + * Add a query modifier to the cursor query + * + * @param name - The query modifier (must start with $, such as $orderby etc) + * @param value - The modifier value. + */ + addQueryModifier(name: string, value: string | boolean | number | Document): this { + if (name[0] !== '$') { + throw new MongoError(`${name} is not a valid query modifier`); + } + + // Strip of the $ + const field = name.substr(1); + + // NOTE: consider some TS magic for this + switch (field) { + case 'comment': + this[kBuiltOptions].comment = value as string | Document; + break; + + case 'explain': + this[kBuiltOptions].explain = value as boolean; + break; + + case 'hint': + this[kBuiltOptions].hint = value as string | Document; + break; + + case 'max': + this[kBuiltOptions].max = value as number; + break; + + case 'maxTimeMS': + this[kBuiltOptions].maxTimeMS = value as number; + break; + + case 'min': + this[kBuiltOptions].min = value as number; + break; + + case 'orderby': + this[kBuiltOptions].sort = formatSort(value as string | Document); + break; + + case 'query': + this[kFilter] = value as Document; + break; + + case 'returnKey': + this[kBuiltOptions].returnKey = value as boolean; + break; + + case 'showDiskLoc': + this[kBuiltOptions].showRecordId = value as boolean; + break; + + default: + throw new TypeError(`invalid query modifier: ${name}`); + } + + return this; + } + + /** + * Add a comment to the cursor query allowing for tracking the comment in the log. + * + * @param value - The comment attached to this query. + */ + comment(value: string): this { + this[kBuiltOptions].comment = value; + return this; + } + + /** + * Set a maxAwaitTimeMS on a tailing cursor query to allow to customize the timeout value for the option awaitData (Only supported on MongoDB 3.2 or higher, ignored otherwise) + * + * @param value - Number of milliseconds to wait before aborting the tailed query. + */ + maxAwaitTimeMS(value: number): this { + if (typeof value !== 'number') { + throw new MongoError('maxAwaitTimeMS must be a number'); + } + + this[kBuiltOptions].maxAwaitTimeMS = value; + return this; + } + + /** + * Set a maxTimeMS on the cursor query, allowing for hard timeout limits on queries (Only supported on MongoDB 2.6 or higher) + * + * @param value - Number of milliseconds to wait before aborting the query. + */ + maxTimeMS(value: number): this { + if (typeof value !== 'number') { + throw new MongoError('maxTimeMS must be a number'); + } + + this[kBuiltOptions].maxTimeMS = value; + return this; + } + + /** + * Sets a field projection for the query. + * + * @param value - The field projection object. + */ + project(value: Document): this { + this[kBuiltOptions].projection = value; + return this; + } + + /** + * Sets the sort order of the cursor query. + * + * @param sort - The key or keys set for the sort. + * @param direction - The direction of the sorting (1 or -1). + */ + sort(sort: Sort | string, direction?: SortDirection): this { + if (this[kBuiltOptions].tailable) { + throw new MongoError('Tailable cursor does not support sorting'); + } + + this[kBuiltOptions].sort = formatSort(sort, direction); + return this; + } + + /** + * Set the collation options for the cursor. + * + * @param value - The cursor collation options (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields). + */ + collation(value: CollationOptions): this { + this[kBuiltOptions].collation = value; + return this; + } + + /** + * Set the limit for the cursor. + * + * @param value - The limit for the cursor query. + */ + limit(value: number): this { + if (this[kBuiltOptions].tailable) { + throw new MongoError('Tailable cursor does not support limit'); + } + + if (typeof value !== 'number') { + throw new MongoError('limit requires an integer'); + } + + this[kBuiltOptions].limit = value; + return this; + } + + /** + * Set the skip for the cursor. + * + * @param value - The skip for the cursor query. + */ + skip(value: number): this { + if (this[kBuiltOptions].tailable) { + throw new MongoError('Tailable cursor does not support skip'); + } + + if (typeof value !== 'number') { + throw new MongoError('skip requires an integer'); + } + + this[kBuiltOptions].skip = value; + return this; + } +} diff --git a/src/cursor/index.ts b/src/cursor/index.ts deleted file mode 100644 index 946a67bea1..0000000000 --- a/src/cursor/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { Cursor, CursorState, CursorStream } from './cursor'; -export { CommandCursor } from './command_cursor'; -export { AggregationCursor } from './aggregation_cursor'; diff --git a/src/db.ts b/src/db.ts index 0c8feaf547..1dba1730ea 100644 --- a/src/db.ts +++ b/src/db.ts @@ -9,7 +9,7 @@ import { getTopology } from './utils'; import { loadAdmin } from './dynamic_loaders'; -import { AggregationCursor, CommandCursor } from './cursor'; +import { AggregationCursor } from './cursor/aggregation_cursor'; import { ObjectId, Code, Document, BSONSerializeOptions, resolveBSONOptions } from './bson'; import { ReadPreference, ReadPreferenceLike } from './read_preference'; import { MongoError } from './error'; @@ -19,7 +19,7 @@ import * as CONSTANTS from './constants'; import { WriteConcern, WriteConcernOptions } from './write_concern'; import { ReadConcern } from './read_concern'; import { Logger, LoggerOptions } from './logger'; -import { AggregateOperation, AggregateOptions } from './operations/aggregate'; +import type { AggregateOptions } from './operations/aggregate'; import { AddUserOperation, AddUserOptions } from './operations/add_user'; import { CollectionsOperation } from './operations/collections'; import { DbStatsOperation, DbStatsOptions } from './operations/stats'; @@ -42,7 +42,7 @@ import { DropDatabaseOptions, DropCollectionOptions } from './operations/drop'; -import { ListCollectionsOperation, ListCollectionsOptions } from './operations/list_collections'; +import { ListCollectionsCursor, ListCollectionsOptions } from './operations/list_collections'; import { ProfilingLevelOperation, ProfilingLevelOptions } from './operations/profiling_level'; import { RemoveUserOperation, RemoveUserOptions } from './operations/remove_user'; import { RenameOperation, RenameOptions } from './operations/rename'; @@ -309,14 +309,13 @@ export class Db implements OperationParent { throw new TypeError('`options` parameter must not be function'); } - options = resolveOptions(this, options); - const cursor = new AggregationCursor( + return new AggregationCursor( + this, getTopology(this), - new AggregateOperation(this, pipeline, options), - options + this.s.namespace, + pipeline, + resolveOptions(this, options) ); - - return cursor; } /** Return the Admin db instance */ @@ -417,15 +416,8 @@ export class Db implements OperationParent { * @param filter - Query to filter collections by * @param options - Optional settings for the command */ - listCollections(filter?: Document, options?: ListCollectionsOptions): CommandCursor { - filter = filter || {}; - options = resolveOptions(this, options); - - return new CommandCursor( - getTopology(this), - new ListCollectionsOperation(this, filter, options), - options - ); + listCollections(filter?: Document, options?: ListCollectionsOptions): ListCollectionsCursor { + return new ListCollectionsCursor(this, filter || {}, resolveOptions(this, options)); } /** @@ -797,7 +789,7 @@ export class Db implements OperationParent { pipeline = []; } - return new ChangeStream(this, pipeline, options); + return new ChangeStream(this, pipeline, resolveOptions(this, options)); } /** Return the db logger */ diff --git a/src/gridfs-stream/download.ts b/src/gridfs-stream/download.ts index a98499e3a6..d4764ff4cc 100644 --- a/src/gridfs-stream/download.ts +++ b/src/gridfs-stream/download.ts @@ -3,11 +3,11 @@ import type { AnyError } from '../error'; import type { Document } from '../bson'; import type { FindOptions } from '../operations/find'; import type { Sort } from '../sort'; -import type { Cursor } from './../cursor/cursor'; import type { Callback } from '../utils'; import type { Collection } from '../collection'; import type { ReadPreference } from '../read_preference'; import type { GridFSBucketWriteStream } from './upload'; +import type { FindCursor } from '../cursor/find_cursor'; /** @public */ export interface GridFSBucketReadStreamOptions { @@ -46,7 +46,7 @@ export interface GridFSBucketReadStreamPrivate { bytesToTrim: number; bytesToSkip: number; chunks: Collection; - cursor?: Cursor; + cursor?: FindCursor; expected: number; files: Collection; filter: Document; @@ -203,7 +203,7 @@ function doRead(stream: GridFSBucketReadStream): void { if (!stream.s.cursor) return; if (!stream.s.file) return; - stream.s.cursor.next((error?: Error, doc?: Document) => { + stream.s.cursor.next((error, doc) => { if (stream.destroyed) { return; } @@ -421,7 +421,7 @@ function handleStartOption( function handleEndOption( stream: GridFSBucketReadStream, doc: Document, - cursor: Cursor, + cursor: FindCursor, options: GridFSBucketReadStreamOptions ) { if (options && options.end != null) { diff --git a/src/gridfs-stream/index.ts b/src/gridfs-stream/index.ts index db5f2285e2..2a6b709faf 100644 --- a/src/gridfs-stream/index.ts +++ b/src/gridfs-stream/index.ts @@ -12,10 +12,10 @@ import type { Document } from '../bson'; import type { Db } from '../db'; import type { ReadPreference } from '../read_preference'; import type { Collection } from '../collection'; -import type { Cursor } from './../cursor/cursor'; import type { FindOptions } from './../operations/find'; import type { Sort } from '../sort'; import type { Logger } from '../logger'; +import type { FindCursor } from '../cursor/find_cursor'; const DEFAULT_GRIDFS_BUCKET_OPTIONS: { bucketName: string; @@ -140,7 +140,7 @@ export class GridFSBucket extends EventEmitter { } /** Convenience wrapper around find on the files collection */ - find(filter: Document, options?: FindOptions): Cursor { + find(filter: Document, options?: FindOptions): FindCursor { filter = filter || {}; options = options || {}; return this.s._filesCollection.find(filter, options); diff --git a/src/index.ts b/src/index.ts index ffcc9e6733..4390753683 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { Instrumentation } from './apm'; -import { Cursor, AggregationCursor, CommandCursor } from './cursor'; +import { AggregationCursor } from './cursor/aggregation_cursor'; import { PromiseProvider } from './promise_provider'; import { Admin } from './admin'; import { MongoClient } from './mongo_client'; @@ -70,8 +70,6 @@ export { ReadPreference, Logger, AggregationCursor, - CommandCursor, - Cursor, GridFSBucket }; @@ -95,7 +93,6 @@ export type { } from './bulk/common'; export type { ChangeStream, - ChangeStreamStream, ChangeStreamOptions, ChangeStreamCursor, ResumeToken, @@ -142,20 +139,13 @@ export type { QueryOptions } from './cmap/wire_protocol/query'; export type { CollationOptions, WriteCommandOptions } from './cmap/wire_protocol/write_command'; export type { CollectionPrivate, CollectionOptions } from './collection'; export type { AggregationCursorOptions } from './cursor/aggregation_cursor'; -export type { CommandCursorOptions } from './cursor/command_cursor'; export type { CursorCloseOptions, - DocumentTransforms, CursorStreamOptions, - CursorStream, - CursorState, - CursorOptions, - FIELDS as CURSOR_FIELDS, - FLAGS as CURSOR_FLAGS, - CursorFlag, - EachCallback, - CursorPrivate -} from './cursor/cursor'; + AbstractCursorOptions, + CURSOR_FLAGS, + CursorFlag +} from './cursor/abstract_cursor'; export type { DbPrivate, DbOptions } from './db'; export type { AutoEncryptionOptions, AutoEncryptionLoggerLevels, AutoEncrypter } from './deps'; export type { AnyError, ErrorDescription } from './error'; @@ -288,11 +278,5 @@ export type { WithTransactionCallback } from './sessions'; export type { TransactionOptions, Transaction, TxnState } from './transactions'; -export type { - Callback, - MongoDBNamespace, - ClientMetadata, - InterruptableAsyncInterval, - ClientMetadataOptions -} from './utils'; +export type { Callback, ClientMetadata, ClientMetadataOptions } from './utils'; export type { WriteConcern, W, WriteConcernOptions } from './write_concern'; diff --git a/src/mongo_client.ts b/src/mongo_client.ts index b26f3d7216..14d45d64e7 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -4,7 +4,7 @@ import { ChangeStream, ChangeStreamOptions } from './change_stream'; import { ReadPreference, ReadPreferenceMode } from './read_preference'; import { MongoError, AnyError } from './error'; import { WriteConcern, WriteConcernOptions } from './write_concern'; -import { maybePromise, MongoDBNamespace, Callback } from './utils'; +import { maybePromise, MongoDBNamespace, Callback, resolveOptions } from './utils'; import { deprecate } from 'util'; import { connect, validOptions } from './operations/connect'; import { PromiseProvider } from './promise_provider'; @@ -535,7 +535,7 @@ export class MongoClient extends EventEmitter implements OperationParent { pipeline = []; } - return new ChangeStream(this, pipeline, options); + return new ChangeStream(this, pipeline, resolveOptions(this, options)); } /** Return the mongo client logger */ diff --git a/src/operations/aggregate.ts b/src/operations/aggregate.ts index a8fee700ba..297a677a86 100644 --- a/src/operations/aggregate.ts +++ b/src/operations/aggregate.ts @@ -66,6 +66,7 @@ export class AggregateOperation extends CommandOperation { - cursor: Cursor; - applySkipLimit: boolean; + collectionName?: string; + query: Document; - constructor(cursor: Cursor, applySkipLimit: boolean, options: CountOptions) { - super(({ s: cursor } as unknown) as Collection, options); + constructor(namespace: MongoDBNamespace, filter: Document, options: CountOptions) { + super(({ s: { namespace: namespace } } as unknown) as Collection, options); - this.cursor = cursor; - this.applySkipLimit = applySkipLimit; + this.collectionName = namespace.collection; + this.query = filter; } execute(server: Server, callback: Callback): void { - const cursor = this.cursor; - const applySkipLimit = this.applySkipLimit; const options = this.options; + const cmd: Document = { + count: this.collectionName, + query: this.query + }; - if (applySkipLimit) { - if (typeof cursor.cursorSkip === 'number') options.skip = cursor.cursorSkip; - if (typeof cursor.cursorLimit === 'number') options.limit = cursor.cursorLimit; + if (typeof options.limit === 'number') { + cmd.limit = options.limit; } - if ( - typeof options.maxTimeMS !== 'number' && - cursor.cmd && - typeof cursor.cmd.maxTimeMS === 'number' - ) { - options.maxTimeMS = cursor.cmd.maxTimeMS; + if (typeof options.skip === 'number') { + cmd.skip = options.skip; } - const finalOptions: BuildCountCommandOptions = { - collectionName: cursor.namespace.collection ?? '' - }; - - finalOptions.skip = options.skip; - finalOptions.limit = options.limit; - finalOptions.hint = options.hint; - finalOptions.maxTimeMS = options.maxTimeMS; + if (typeof options.hint !== 'undefined') { + cmd.hint = options.hint; + } - let command; - try { - command = buildCountCommand(cursor, cursor.cmd.query, finalOptions); - } catch (err) { - return callback(err); + if (typeof options.maxTimeMS === 'number') { + cmd.maxTimeMS = options.maxTimeMS; } - super.executeCommand(server, command, (err, result) => { + super.executeCommand(server, cmd, (err, result) => { callback(err, result ? result.n : 0); }); } } -/** - * Build the count command. - * - * @param collectionOrCursor - an instance of a collection or cursor - * @param query - The query for the count. - * @param options - Optional settings. See Collection.prototype.count and Cursor.prototype.count for a list of options. - */ -function buildCountCommand( - collectionOrCursor: Collection | Cursor, - query: Document, - options: BuildCountCommandOptions -) { - const skip = options.skip; - const limit = options.limit; - let hint = options.hint; - const maxTimeMS = options.maxTimeMS; - query = query || {}; - - // Final query - const cmd: Document = { - count: options.collectionName, - query: query - }; - - if (isCursor(collectionOrCursor)) { - // collectionOrCursor is a cursor - if (collectionOrCursor.options.hint) { - hint = collectionOrCursor.options.hint; - } else if (collectionOrCursor.cmd.hint) { - hint = collectionOrCursor.cmd.hint; - } - decorateWithCollation(cmd, collectionOrCursor, collectionOrCursor.cmd); - } else { - decorateWithCollation(cmd, collectionOrCursor, options); - } - - // Add limit, skip and maxTimeMS if defined - if (typeof skip === 'number') cmd.skip = skip; - if (typeof limit === 'number') cmd.limit = limit; - if (typeof maxTimeMS === 'number') cmd.maxTimeMS = maxTimeMS; - if (hint) cmd.hint = hint; - - // Do we have a readConcern specified - decorateWithReadConcern(cmd, collectionOrCursor); - - return cmd; -} - -function isCursor(c: Collection | Cursor): c is Cursor { - return 'numberOfRetries' in c.s && 'undefined' !== typeof c.s.numberOfRetries; -} - defineAspects(CountOperation, [Aspect.READ_OPERATION, Aspect.RETRYABLE]); diff --git a/src/operations/execute_operation.ts b/src/operations/execute_operation.ts index 50f60b50f8..18d418cff6 100644 --- a/src/operations/execute_operation.ts +++ b/src/operations/execute_operation.ts @@ -6,6 +6,7 @@ import { ServerType } from '../sdam/common'; import type { Server } from '../sdam/server'; import type { Topology } from '../sdam/topology'; import type { ClientSession } from '../sessions'; +import type { Document } from '../bson'; const MMAPv1_RETRY_WRITES_ERROR_CODE = 20; const MMAPv1_RETRY_WRITES_ERROR_MESSAGE = @@ -21,6 +22,16 @@ type OptionsFromOperation = TOperation extends OperationBase { hint?: Hint; constructor( - collection: Collection, + collection: Collection | undefined, ns: MongoDBNamespace, filter: Document = {}, options: FindOptions = {} @@ -137,11 +137,28 @@ export class FindOperation extends CommandOperation { } if (typeof options.limit === 'number') { - findCommand.limit = options.limit; + if (options.limit < 0 && maxWireVersion(server) >= 4) { + findCommand.limit = Math.abs(options.limit); + findCommand.singleBatch = true; + } else { + findCommand.limit = options.limit; + } } if (typeof options.batchSize === 'number') { - findCommand.batchSize = options.batchSize; + if (options.batchSize < 0) { + if ( + options.limit && + options.limit !== 0 && + Math.abs(options.batchSize) < Math.abs(options.limit) + ) { + findCommand.limit = Math.abs(options.batchSize); + } + + findCommand.singleBatch = true; + } else { + findCommand.batchSize = Math.abs(options.batchSize); + } } if (typeof options.singleBatch === 'boolean') { diff --git a/src/operations/find_one.ts b/src/operations/find_one.ts index 047d08458f..b7f83c971b 100644 --- a/src/operations/find_one.ts +++ b/src/operations/find_one.ts @@ -30,7 +30,7 @@ export class FindOneOperation extends CommandOperation { // Return the item cursor.next((err, item) => { if (err != null) return callback(new MongoError(err)); - callback(undefined, item); + callback(undefined, item || undefined); }); } catch (e) { callback(e); diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index a57c5548ac..b4ea309cfe 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -1,7 +1,13 @@ import { indexInformation, IndexInformationOptions } from './common_functions'; import { OperationBase, Aspect, defineAspects } from './operation'; import { MongoError } from '../error'; -import { maxWireVersion, parseIndexOptions, MongoDBNamespace, Callback } from '../utils'; +import { + maxWireVersion, + parseIndexOptions, + MongoDBNamespace, + Callback, + getTopology +} from '../utils'; import { CommandOperation, CommandOperationOptions, OperationParent } from './command'; import { ReadPreference } from '../read_preference'; import type { Server } from '../sdam/server'; @@ -10,6 +16,9 @@ import type { Collection } from '../collection'; import type { Db } from '../db'; import type { CollationOptions } from '../cmap/wire_protocol/write_command'; import type { FindOptions } from './find'; +import { AbstractCursor } from '../cursor/abstract_cursor'; +import type { ClientSession } from '../sessions'; +import { executeOperation, ExecutionResult } from './execute_operation'; const LIST_INDEXES_WIRE_VERSION = 3; const VALID_INDEX_OPTIONS = new Set([ @@ -349,6 +358,34 @@ export class ListIndexesOperation extends CommandOperation): void { + const operation = new ListIndexesOperation(this.parent, { + ...this.cursorOptions, + ...this.options, + session + }); + + executeOperation(getTopology(this.parent), operation, (err, response) => { + if (err || response == null) return callback(err); + + // TODO: NODE-2882 + callback(undefined, { server: operation.server, session, response }); + }); + } +} + /** @internal */ export class IndexExistsOperation extends OperationBase { collection: Collection; diff --git a/src/operations/list_collections.ts b/src/operations/list_collections.ts index 2900eaa083..4690b270df 100644 --- a/src/operations/list_collections.ts +++ b/src/operations/list_collections.ts @@ -1,30 +1,16 @@ import { CommandOperation, CommandOperationOptions } from './command'; import { Aspect, defineAspects } from './operation'; -import { maxWireVersion, Callback } from '../utils'; +import { maxWireVersion, Callback, getTopology } from '../utils'; import * as CONSTANTS from '../constants'; import type { Document } from '../bson'; import type { Server } from '../sdam/server'; import type { Db } from '../db'; -import type { DocumentTransforms } from '../cursor/cursor'; +import { AbstractCursor } from '../cursor/abstract_cursor'; +import type { ClientSession } from '../sessions'; +import { executeOperation, ExecutionResult } from './execute_operation'; const LIST_COLLECTIONS_WIRE_VERSION = 3; -function listCollectionsTransforms(databaseName: string): DocumentTransforms { - const matching = `${databaseName}.`; - - return { - doc(doc) { - const index = doc.name.indexOf(matching); - // Remove database name if available - if (doc.name && index === 0) { - doc.name = doc.name.substr(index + matching.length); - } - - return doc; - } - }; -} - /** @public */ export interface ListCollectionsOptions extends CommandOperationOptions { /** Since 4.0: If true, will only return the collection name in the response, and will omit additional info */ @@ -40,7 +26,7 @@ export class ListCollectionsOperation extends CommandOperation { + const matching = `${databaseName}.`; + const index = doc.name.indexOf(matching); + // Remove database name if available + if (doc.name && index === 0) { + doc.name = doc.name.substr(index + matching.length); + } + + return doc; + }; + server.query( `${databaseName}.${CONSTANTS.SYSTEM_NAMESPACE_COLLECTION}`, { query: filter }, { batchSize: this.batchSize || 1000 }, (err, result) => { if (result && result.documents && Array.isArray(result.documents)) { - result.documents = result.documents.map(transforms.doc); + result.documents = result.documents.map(documentTransform); } callback(err, result); @@ -106,4 +102,32 @@ export class ListCollectionsOperation extends CommandOperation): void { + const operation = new ListCollectionsOperation(this.parent, this.filter, { + ...this.cursorOptions, + ...this.options, + session + }); + + executeOperation(getTopology(this.parent), operation, (err, response) => { + if (err || response == null) return callback(err); + + // TODO: NODE-2882 + callback(undefined, { server: operation.server, session, response }); + }); + } +} + defineAspects(ListCollectionsOperation, [Aspect.READ_OPERATION, Aspect.RETRYABLE]); diff --git a/src/sdam/topology.ts b/src/sdam/topology.ts index cea8938cfd..d981f64a8b 100644 --- a/src/sdam/topology.ts +++ b/src/sdam/topology.ts @@ -4,7 +4,6 @@ import { ReadPreference, ReadPreferenceLike } from '../read_preference'; import { ServerDescription } from './server_description'; import { TopologyDescription } from './topology_description'; import { Server, ServerOptions } from './server'; -import { Cursor } from '../cursor'; import { ClientSession, ServerSessionPool, @@ -23,7 +22,6 @@ import { makeClientMetadata, emitDeprecatedOptionWarning, ClientMetadata, - MongoDBNamespace, Callback } from '../utils'; import { @@ -54,8 +52,6 @@ import type { Transaction } from '../transactions'; import type { CloseOptions } from '../cmap/connection_pool'; import type { LoggerOptions } from '../logger'; import { DestroyOptions, Connection } from '../cmap/connection'; -import { RunCommandOperation } from '../operations/run_command'; -import type { CursorOptions } from '../cursor/cursor'; import type { MongoClientOptions } from '../mongo_client'; // Global state @@ -114,8 +110,6 @@ export interface TopologyPrivate { serverSelectionTimeoutMS: number; heartbeatFrequencyMS: number; minHeartbeatFrequencyMS: number; - /** allow users to override the cursor factory */ - Cursor: typeof Cursor; /** A map of server instances to normalized addresses */ servers: Map; /** Server Session Pool */ @@ -155,7 +149,6 @@ export interface TopologyOptions extends ServerOptions, BSONSerializeOptions, Lo minHeartbeatFrequencyMS: number; /** The name of the replica set to connect to */ replicaSet?: string; - cursorFactory: typeof Cursor; srvHost?: string; srvPoller?: SrvPoller; /** Indicates that a client should directly connect to a node without attempting to discover its topology type */ @@ -268,8 +261,6 @@ export class Topology extends EventEmitter { serverSelectionTimeoutMS: options.serverSelectionTimeoutMS, heartbeatFrequencyMS: options.heartbeatFrequencyMS, minHeartbeatFrequencyMS: options.minHeartbeatFrequencyMS, - // allow users to override the cursor factory - Cursor: options.cursorFactory || Cursor, // a map of server instances to normalized addresses servers: new Map(), // Server Session Pool @@ -682,26 +673,6 @@ export class Topology extends EventEmitter { if (typeof callback === 'function') callback(undefined, true); } - /** - * Create a new cursor - * - * @param ns - The MongoDB fully qualified namespace (ex: db1.collection1) - * @param cmd - Can be either a command returning a cursor or a cursorId - * @param options - Options for the cursor - */ - cursor(ns: string, cmd: Document, options?: CursorOptions): Cursor { - options = options || {}; - const topology = options.topology || this; - const CursorClass = options.cursorFactory ?? this.s.Cursor; - ReadPreference.translate(options); - - return new CursorClass( - topology, - new RunCommandOperation({ s: { namespace: MongoDBNamespace.fromString(ns) } }, cmd, options), - options - ); - } - get clientMetadata(): ClientMetadata { return this.s.options.metadata; } diff --git a/src/sessions.ts b/src/sessions.ts index ca89d82dc1..5c90b35d35 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -17,10 +17,10 @@ import { } from './utils'; import type { Topology } from './sdam/topology'; import type { MongoClientOptions } from './mongo_client'; -import type { Cursor } from './cursor/cursor'; import type { WriteCommandOptions } from './cmap/wire_protocol/write_command'; import { executeOperation } from './operations/execute_operation'; import { RunAdminCommandOperation } from './operations/run_command'; +import type { AbstractCursor } from './cursor/abstract_cursor'; const minWireVersionForShardedTransactions = 8; @@ -45,7 +45,7 @@ export interface ClientSessionOptions { /** The default TransactionOptions to use for transactions started on this session. */ defaultTransactionOptions?: TransactionOptions; - owner: symbol | Cursor; + owner: symbol | AbstractCursor; explicit?: boolean; initialClusterTime?: ClusterTime; } @@ -70,7 +70,7 @@ class ClientSession extends EventEmitter { clusterTime?: ClusterTime; operationTime?: Timestamp; explicit: boolean; - owner: symbol | Cursor; // TODO - change to AbstractCursor + owner: symbol | AbstractCursor; defaultTransactionOptions: TransactionOptions; transaction: Transaction; [kServerSession]?: ServerSession; diff --git a/src/utils.ts b/src/utils.ts index 0c146835ae..94c6416b88 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -18,7 +18,6 @@ import { Document, resolveBSONOptions } from './bson'; import type { IndexSpecification, IndexDirection } from './operations/indexes'; import type { Explain } from './explain'; import type { MongoClient } from './mongo_client'; -import type { Cursor } from './cursor/cursor'; import type { CommandOperationOptions, OperationParent } from './operations/command'; import { ReadPreference } from './read_preference'; @@ -405,7 +404,7 @@ export function isPromiseLike( */ export function decorateWithCollation( command: Document, - target: MongoClient | Db | Collection | Cursor, + target: MongoClient | Db | Collection, options: AnyOptions ): void { const capabilities = getTopology(target).capabilities(); @@ -463,7 +462,7 @@ export function decorateWithExplain(command: Document, explain: Explain): Docume * if the topology cannot be found. * @internal */ -export function getTopology(provider: MongoClient | Db | Collection | Cursor): Topology { +export function getTopology(provider: MongoClient | Db | Collection): Topology { if (`topology` in provider && provider.topology) { return provider.topology; } else if ('client' in provider.s && provider.s.client.topology) { diff --git a/test/functional/abstract_cursor.test.js b/test/functional/abstract_cursor.test.js new file mode 100644 index 0000000000..b813d6acbc --- /dev/null +++ b/test/functional/abstract_cursor.test.js @@ -0,0 +1,137 @@ +'use strict'; +const { expect } = require('chai'); +const { filterForCommands } = require('./shared'); + +function withClientV2(callback) { + return function testFunction(done) { + const client = this.configuration.newClient({ monitorCommands: true }); + client.connect(err => { + if (err) return done(err); + this.defer(() => client.close()); + + try { + callback.call(this, client, done); + } catch (err) { + done(err); + } + }); + }; +} + +describe('AbstractCursor', function () { + before( + withClientV2((client, done) => { + const docs = [{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }, { a: 5 }, { a: 6 }]; + const coll = client.db().collection('find_cursor'); + coll.drop(() => coll.insertMany(docs, done)); + }) + ); + + context('#next', function () { + it( + 'should support a batch size', + withClientV2(function (client, done) { + const commands = []; + client.on('commandStarted', filterForCommands(['getMore'], commands)); + + const coll = client.db().collection('find_cursor'); + const cursor = coll.find({}, { batchSize: 2 }); + this.defer(() => cursor.close()); + + cursor.toArray((err, docs) => { + expect(err).to.not.exist; + expect(docs).to.have.length(6); + expect(commands).to.have.length(3); + done(); + }); + }) + ); + }); + + context('#close', function () { + it( + 'should a killCursors command when closed before completely iterated', + withClientV2(function (client, done) { + const commands = []; + client.on('commandStarted', filterForCommands(['killCursors'], commands)); + + const coll = client.db().collection('find_cursor'); + const cursor = coll.find({}, { batchSize: 2 }); + cursor.next(err => { + expect(err).to.not.exist; + cursor.close(err => { + expect(err).to.not.exist; + expect(commands).to.have.length(1); + done(); + }); + }); + }) + ); + + it( + 'should not send a killCursors command when closed after completely iterated', + withClientV2(function (client, done) { + const commands = []; + client.on('commandStarted', filterForCommands(['killCursors'], commands)); + + const coll = client.db().collection('find_cursor'); + const cursor = coll.find({}, { batchSize: 2 }); + cursor.toArray(err => { + expect(err).to.not.exist; + + cursor.close(err => { + expect(err).to.not.exist; + expect(commands).to.have.length(0); + done(); + }); + }); + }) + ); + + it( + 'should not send a killCursors command when closed before initialization', + withClientV2(function (client, done) { + const commands = []; + client.on('commandStarted', filterForCommands(['killCursors'], commands)); + + const coll = client.db().collection('find_cursor'); + const cursor = coll.find({}, { batchSize: 2 }); + cursor.close(err => { + expect(err).to.not.exist; + expect(commands).to.have.length(0); + done(); + }); + }) + ); + }); + + context('#forEach', function () { + it( + 'should iterate each document in a cursor', + withClientV2(function (client, done) { + const coll = client.db().collection('find_cursor'); + const cursor = coll.find({}, { batchSize: 2 }); + + const bag = []; + cursor.forEach( + doc => bag.push(doc), + err => { + expect(err).to.not.exist; + expect(bag).to.have.lengthOf(6); + done(); + } + ); + }) + ); + }); + + context('sessions', function () { + it( + 'should end a session after close if the cursor owns the session', + withClientV2(function (client, done) { + // TBD + done(); + }) + ); + }); +}); diff --git a/test/functional/aggregation.test.js b/test/functional/aggregation.test.js index f80e527da0..cc2e3aa9fb 100644 --- a/test/functional/aggregation.test.js +++ b/test/functional/aggregation.test.js @@ -395,7 +395,7 @@ describe('Aggregation', function () { }, test: function (done) { - var client = this.configuration.newClient({ w: 1 }, { maxPoolSize: 1 }), + var client = this.configuration.newClient({ maxPoolSize: 1 }), databaseName = this.configuration.db; // LINE var MongoClient = require('mongodb').MongoClient; @@ -891,7 +891,7 @@ describe('Aggregation', function () { try { // Execute aggregate, notice the pipeline is expressed as an Array - collection.aggregate( + const cursor = collection.aggregate( [ { $project: { @@ -911,6 +911,8 @@ describe('Aggregation', function () { cursor: 1 } ); + + cursor.next(); } catch (err) { client.close(done); return; diff --git a/test/functional/apm.test.js b/test/functional/apm.test.js index 5f5899cc37..11f4a8527e 100644 --- a/test/functional/apm.test.js +++ b/test/functional/apm.test.js @@ -871,7 +871,7 @@ describe('APM', function () { } // otherwise compare the values - expect(maybeLong(actual[key])).to.deep.equal(expected[key]); + expect(maybeLong(actual[key]), key).to.deep.equal(expected[key]); }); } diff --git a/test/functional/causal_consistency.test.js b/test/functional/causal_consistency.test.js index 8e9e6320c3..d187b3e21e 100644 --- a/test/functional/causal_consistency.test.js +++ b/test/functional/causal_consistency.test.js @@ -74,7 +74,8 @@ describe('Causal Consistency', function () { expect(test.commands.succeeded).to.have.length(1); const lastReply = test.commands.succeeded[0].reply; - expect(session.operationTime).to.equal(lastReply.operationTime); + const maybeLong = val => (typeof val.equals === 'function' ? val.toNumber() : val); + expect(maybeLong(session.operationTime)).to.equal(maybeLong(lastReply.operationTime)); }); } }); diff --git a/test/functional/change_stream.test.js b/test/functional/change_stream.test.js index f0229d3ca7..0ffff05eb4 100644 --- a/test/functional/change_stream.test.js +++ b/test/functional/change_stream.test.js @@ -1832,45 +1832,6 @@ describe('Change Streams', function () { } }); - it('should emit close event after error event', { - metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } }, - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient(); - - client.connect((err, client) => { - expect(err).to.not.exist; - this.defer(() => client.close()); - - const db = client.db('integration_tests'); - const coll = db.collection('event_test'); - - // This will cause an error because the _id will be projected out, which causes the following error: - // "A change stream document has been received that lacks a resume token (_id)." - const changeStream = coll.watch([{ $project: { _id: false } }]); - changeStream.on('change', changeDoc => { - expect(changeDoc).to.be.null; - }); - - let errored = false; - changeStream.on('error', err => { - expect(err).to.exist; - errored = true; - }); - - changeStream.once('close', () => { - expect(errored).to.be.true; - done(); - }); - - // Trigger the first database event - waitForStarted(changeStream, () => { - this.defer(coll.insertOne({ a: 1 })); - }); - }); - } - }); - describe('should properly handle a changeStream event being processed mid-close', function () { let client, coll, changeStream; @@ -1938,25 +1899,28 @@ describe('Change Streams', function () { it('when invoked with callbacks', { metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } }, test: function (done) { + const ops = []; changeStream.next(() => { changeStream.next(() => { - this.defer(lastWrite()); + ops.push(lastWrite()); + + // explicitly close the change stream after the write has begun + ops.push(changeStream.close()); changeStream.next(err => { try { - expect(err).property('message').to.equal('ChangeStream is closed'); - done(); + expect(err) + .property('message') + .to.match(/ChangeStream is closed/); + Promise.all(ops).then(() => done(), done); } catch (e) { done(e); } }); - - // explicitly close the change stream after the write has begun - this.defer(changeStream.close()); }); }); - this.defer(write().catch(() => {})); + ops.push(write().catch(() => {})); } }); @@ -2192,10 +2156,10 @@ describe('Change Streams', function () { it('must return the postBatchResumeToken from the current command response', function () { const manager = new MockServerManager(this.configuration, { aggregate: (function* () { - yield { numDocuments: 0, postBatchResumeToken: true }; + yield { numDocuments: 0, postBatchResumeToken: true, cursor: { firstBatch: [] } }; })(), getMore: (function* () { - yield { numDocuments: 1, postBatchResumeToken: true }; + yield { numDocuments: 1, postBatchResumeToken: true, cursor: { nextBatch: [{}] } }; })() }); @@ -2795,7 +2759,6 @@ describe('Change Stream Resume Error Tests', function () { const bucket = []; d.on('data', data => { bucket.push(data.fullDocument.x); - console.log({ bucket }); if (bucket.length === 2) { expect(bucket[0]).to.be(1); expect(bucket[0]).to.be(2); @@ -2808,7 +2771,6 @@ describe('Change Stream Resume Error Tests', function () { expect(err).to.not.exist; expect(result).to.exist; triggerResumableError(changeStream, 250, () => { - console.log('triggered error'); collection.insertOne({ x: 2 }, (err, result) => { expect(err).to.not.exist; expect(result).to.exist; diff --git a/test/functional/collations.test.js b/test/functional/collations.test.js index b4662fd194..99350cdf67 100644 --- a/test/functional/collations.test.js +++ b/test/functional/collations.test.js @@ -107,7 +107,7 @@ describe('Collation', function () { request.reply(primary[0]); } else if (doc.aggregate) { commandResult = doc; - request.reply({ ok: 1 }); + request.reply({ ok: 1, cursor: { id: 0, firstBatch: [], ns: configuration.db } }); } else if (doc.endSessions) { request.reply({ ok: 1 }); } @@ -296,7 +296,7 @@ describe('Collation', function () { request.reply(primary[0]); } else if (doc.find) { commandResult = doc; - request.reply({ ok: 1 }); + request.reply({ ok: 1, cursor: { id: 0, firstBatch: [] } }); } else if (doc.endSessions) { request.reply({ ok: 1 }); } @@ -332,7 +332,7 @@ describe('Collation', function () { request.reply(primary[0]); } else if (doc.find) { commandResult = doc; - request.reply({ ok: 1 }); + request.reply({ ok: 1, cursor: { id: 0, firstBatch: [] } }); } else if (doc.endSessions) { request.reply({ ok: 1 }); } @@ -370,7 +370,7 @@ describe('Collation', function () { request.reply(primary[0]); } else if (doc.find) { commandResult = doc; - request.reply({ ok: 1 }); + request.reply({ ok: 1, cursor: { id: 0, firstBatch: [] } }); } else if (doc.endSessions) { request.reply({ ok: 1 }); } diff --git a/test/functional/command_write_concern.test.js b/test/functional/command_write_concern.test.js index d4b6f3b23f..df1533c895 100644 --- a/test/functional/command_write_concern.test.js +++ b/test/functional/command_write_concern.test.js @@ -86,7 +86,7 @@ describe('Command Write Concern', function () { request.reply(primary[0]); } else if (doc.aggregate) { commandResult = doc; - request.reply({ ok: 1 }); + request.reply({ ok: 1, cursor: { id: 0, firstBatch: [], ns: configuration.db } }); } else if (doc.endSessions) { request.reply({ ok: 1 }); } diff --git a/test/functional/core/cursor.test.js b/test/functional/core/cursor.test.js index b10c4e5950..8c47eb170d 100644 --- a/test/functional/core/cursor.test.js +++ b/test/functional/core/cursor.test.js @@ -3,6 +3,8 @@ const expect = require('chai').expect; const f = require('util').format; const setupDatabase = require('../shared').setupDatabase; +const { FindCursor } = require('../../../src/cursor/find_cursor'); +const { MongoDBNamespace } = require('../../../src/utils'); describe('Cursor tests', function () { before(function () { @@ -37,11 +39,12 @@ describe('Cursor tests', function () { expect(results.n).to.equal(3); // Execute find - var cursor = topology.cursor(ns, { - find: 'cursor1', - filter: {}, - batchSize: 2 - }); + const cursor = new FindCursor( + topology, + MongoDBNamespace.fromString(ns), + {}, + { batchSize: 2 } + ); // Execute next cursor.next((nextCursorErr, nextCursorD) => { @@ -94,11 +97,12 @@ describe('Cursor tests', function () { expect(results.n).to.equal(5); // Execute find - const cursor = topology.cursor(ns, { - find: 'cursor2', - filter: {}, - batchSize: 5 - }); + const cursor = new FindCursor( + topology, + MongoDBNamespace.fromString(ns), + {}, + { batchSize: 5 } + ); // Execute next cursor.next((nextCursorErr, nextCursorD) => { @@ -154,7 +158,12 @@ describe('Cursor tests', function () { expect(results.n).to.equal(1); // Execute find - const cursor = topology.cursor(ns, { find: 'cursor3', filter: {}, batchSize: 5 }); + const cursor = new FindCursor( + topology, + MongoDBNamespace.fromString(ns), + {}, + { batchSize: 2 } + ); // Execute next cursor.next((nextCursorErr, nextCursorD) => { @@ -167,8 +176,8 @@ describe('Cursor tests', function () { expect(secondCursorD).to.not.exist; cursor.next((thirdCursorErr, thirdCursorD) => { - expect(thirdCursorErr).to.be.ok; - expect(thirdCursorD).to.be.undefined; + expect(thirdCursorErr).to.not.exist; + expect(thirdCursorD).to.be.null; // Destroy the server connection server.destroy(done); }); @@ -210,7 +219,12 @@ describe('Cursor tests', function () { expect(results.n).to.equal(3); // Execute find - const cursor = topology.cursor(ns, { find: 'cursor4', filter: {}, batchSize: 2 }); + const cursor = new FindCursor( + topology, + MongoDBNamespace.fromString(ns), + {}, + { batchSize: 2 } + ); // Execute next cursor.next((nextCursorErr, nextCursorD) => { @@ -266,7 +280,12 @@ describe('Cursor tests', function () { expect(results.n).to.equal(3); // Execute find - const cursor = topology.cursor(ns, { find: 'cursor4', filter: {}, batchSize: 2 }); + const cursor = new FindCursor( + topology, + MongoDBNamespace.fromString(ns), + {}, + { batchSize: 2 } + ); // Execute next cursor.next((nextCursorErr, nextCursorD) => { @@ -279,12 +298,11 @@ describe('Cursor tests', function () { expect(secondCursorD.a).to.equal(2); // Kill cursor - cursor.kill(() => { + cursor.close(() => { // Should error out cursor.next((thirdCursorErr, thirdCursorD) => { - expect(thirdCursorErr).to.exist; - expect(thirdCursorErr.message).to.equal('Cursor is closed'); - expect(thirdCursorD).to.not.exist; + expect(thirdCursorErr).to.not.exist; + expect(thirdCursorD).to.be.null; // Destroy the server connection server.destroy(done); @@ -324,7 +342,12 @@ describe('Cursor tests', function () { expect(results.n).to.equal(3); // Execute find - var cursor = _server.cursor(ns, { find: 'cursor5', filter: {}, batchSize: 2 }); + const cursor = new FindCursor( + _server, + MongoDBNamespace.fromString(ns), + {}, + { batchSize: 2 } + ); // Execute next cursor.next(function (nextCursorErr, nextCursorD) { diff --git a/test/functional/core/extend_cursor.test.js b/test/functional/core/extend_cursor.test.js deleted file mode 100644 index f032710a9a..0000000000 --- a/test/functional/core/extend_cursor.test.js +++ /dev/null @@ -1,89 +0,0 @@ -'use strict'; - -const { expect } = require('chai'); -const { Cursor } = require('../../../src/cursor'); - -describe('Extend cursor tests', function () { - it('should correctly extend the cursor with custom implementation', { - metadata: { - requires: { topology: ['single'], mongodb: '>=3.2' } - }, - - test: function (done) { - var self = this; - const config = this.configuration; - - // Create an extended cursor that adds a toArray function - class ExtendedCursor extends Cursor { - constructor(topology, ns, cmd, options) { - super(topology, ns, cmd, options); - var extendedCursorSelf = this; - - // Resolve all the next - var getAllNexts = function (items, callback) { - extendedCursorSelf.next(function (err, item) { - if (err) return callback(err); - if (item === null) return callback(null, null); - items.push(item); - getAllNexts(items, callback); - }); - }; - - // Adding a toArray function to the cursor - this.toArray = function (callback) { - var items = []; - - getAllNexts(items, function (err) { - if (err) return callback(err, null); - callback(null, items); - }); - }; - } - } - - // Attempt to connect, adding a custom cursor creator - const topology = config.newTopology(this.configuration.host, this.configuration.port, { - cursorFactory: ExtendedCursor - }); - - topology.connect(err => { - expect(err).to.not.exist; - this.defer(() => topology.close()); - - topology.selectServer('primary', (err, server) => { - expect(err).to.not.exist; - - const ns = `${self.configuration.db}.inserts_extend_cursors`; - // Execute the write - server.insert( - ns, - [{ a: 1 }, { a: 2 }, { a: 3 }], - { - writeConcern: { w: 1 }, - ordered: true - }, - (err, results) => { - expect(err).to.not.exist; - expect(results).property('n').to.equal(3); - - // Execute find - const cursor = topology.cursor(ns, { find: 'inserts_extend_cursors', filter: {} }); - - // Force a single - // Logger.setLevel('debug'); - // Set the batch size - cursor.batchSize = 2; - // Execute next - cursor.toArray((cursorErr, cursorItems) => { - expect(cursorErr).to.not.exist; - expect(cursorItems.length).to.equal(3); - // Destroy the connection - server.destroy(done); - }); - } - ); - }); - }); - } - }); -}); diff --git a/test/functional/core/tailable_cursor.test.js b/test/functional/core/tailable_cursor.test.js index 5bda62d36b..9450210953 100644 --- a/test/functional/core/tailable_cursor.test.js +++ b/test/functional/core/tailable_cursor.test.js @@ -1,4 +1,8 @@ 'use strict'; + +const { MongoDBNamespace } = require('../../../src/utils'); +const { FindCursor } = require('../../../src/cursor/find_cursor'); + const expect = require('chai').expect; const setupDatabase = require('../shared').setupDatabase; @@ -45,13 +49,12 @@ describe('Tailable cursor tests', function () { expect(results.n).to.equal(1); // Execute find - const cursor = topology.cursor(ns, { - find: 'cursor_tailable', - filter: {}, - batchSize: 2, - tailable: true, - awaitData: true - }); + const cursor = new FindCursor( + topology, + MongoDBNamespace.fromString(ns), + {}, + { batchSize: 2, tailable: true, awaitData: true } + ); // Execute next cursor.next((cursorErr, cursorD) => { @@ -67,7 +70,7 @@ describe('Tailable cursor tests', function () { server.destroy(done); }); - setTimeout(() => cursor.kill(), 300); + setTimeout(() => cursor.close(), 300); }); } ); diff --git a/test/functional/core/undefined.test.js b/test/functional/core/undefined.test.js index 164c1ff5bf..83826cc54a 100644 --- a/test/functional/core/undefined.test.js +++ b/test/functional/core/undefined.test.js @@ -3,6 +3,8 @@ const { expect } = require('chai'); const { format: f } = require('util'); const { ObjectId } = require('bson'); +const { FindCursor } = require('../../../src/cursor/find_cursor'); +const { MongoDBNamespace } = require('../../../src/utils'); describe('A server', function () { it('should correctly execute insert culling undefined', { @@ -39,11 +41,12 @@ describe('A server', function () { expect(results.n).to.eql(1); // Execute find - var cursor = topology.cursor(ns, { - find: 'insert1', - filter: { _id: objectId }, - batchSize: 2 - }); + const cursor = new FindCursor( + topology, + MongoDBNamespace.fromString(ns), + { _id: objectId }, + { batchSize: 2 } + ); // Execute next cursor.next((nextErr, d) => { @@ -99,11 +102,12 @@ describe('A server', function () { expect(results.n).to.eql(1); // Execute find - const cursor = topology.cursor(ns, { - find: 'update1', - filter: { _id: objectId }, - batchSize: 2 - }); + const cursor = new FindCursor( + topology, + MongoDBNamespace.fromString(ns), + { _id: objectId }, + { batchSize: 2 } + ); // Execute next cursor.next((nextErr, d) => { diff --git a/test/functional/crud_api.test.js b/test/functional/crud_api.test.js index a199453aa1..37be842016 100644 --- a/test/functional/crud_api.test.js +++ b/test/functional/crud_api.test.js @@ -32,25 +32,29 @@ describe('CRUD API', function () { // // Cursor // -------------------------------------------------- - var cursor = db.collection('t').find({}); - // Possible methods on the the cursor instance - cursor - .filter({ a: 1 }) - .addCursorFlag('noCursorTimeout', true) - .addQueryModifier('$comment', 'some comment') - .batchSize(2) - .comment('some comment 2') - .limit(2) - .maxTimeMS(50) - .project({ a: 1 }) - .skip(0) - .sort({ a: 1 }); + const makeCursor = () => { + // Possible methods on the the cursor instance + return db + .collection('t') + .find({}) + .filter({ a: 1 }) + .addCursorFlag('noCursorTimeout', true) + .addQueryModifier('$comment', 'some comment') + .batchSize(2) + .comment('some comment 2') + .limit(2) + .maxTimeMS(50) + .project({ a: 1 }) + .skip(0) + .sort({ a: 1 }); + }; // // Exercise count method // ------------------------------------------------- var countMethod = function () { // Execute the different methods supported by the cursor + const cursor = makeCursor(); cursor.count(function (err, count) { expect(err).to.not.exist; test.equal(2, count); @@ -64,20 +68,24 @@ describe('CRUD API', function () { var eachMethod = function () { var count = 0; - cursor.each(function (err, doc) { - expect(err).to.not.exist; - if (doc) count = count + 1; - if (doc == null) { + const cursor = makeCursor(); + cursor.forEach( + () => { + count = count + 1; + }, + err => { + expect(err).to.not.exist; test.equal(2, count); toArrayMethod(); } - }); + ); }; // // Exercise toArray // ------------------------------------------------- var toArrayMethod = function () { + const cursor = makeCursor(); cursor.toArray(function (err, docs) { expect(err).to.not.exist; test.equal(2, docs.length); @@ -89,16 +97,16 @@ describe('CRUD API', function () { // Exercise next method // ------------------------------------------------- var nextMethod = function () { - var clonedCursor = cursor.clone(); - clonedCursor.next(function (err, doc) { + const cursor = makeCursor(); + cursor.next(function (err, doc) { expect(err).to.not.exist; test.ok(doc != null); - clonedCursor.next(function (err, doc) { + cursor.next(function (err, doc) { expect(err).to.not.exist; test.ok(doc != null); - clonedCursor.next(function (err, doc) { + cursor.next(function (err, doc) { expect(err).to.not.exist; expect(doc).to.not.exist; streamMethod(); @@ -112,13 +120,13 @@ describe('CRUD API', function () { // ------------------------------------------------- var streamMethod = function () { var count = 0; - var clonedCursor = cursor.clone(); - const stream = clonedCursor.stream(); + const cursor = makeCursor(); + const stream = cursor.stream(); stream.on('data', function () { count = count + 1; }); - stream.once('end', function () { + cursor.once('close', function () { test.equal(2, count); explainMethod(); }); @@ -128,8 +136,8 @@ describe('CRUD API', function () { // Explain method // ------------------------------------------------- var explainMethod = function () { - var clonedCursor = cursor.clone(); - clonedCursor.explain(function (err, result) { + const cursor = makeCursor(); + cursor.explain(function (err, result) { expect(err).to.not.exist; test.ok(result != null); @@ -153,7 +161,7 @@ describe('CRUD API', function () { test: function (done) { var configuration = this.configuration; - var client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); + var client = configuration.newClient({ maxPoolSize: 1 }); client.connect(function (err, client) { var db = client.db(configuration.db); @@ -223,14 +231,16 @@ describe('CRUD API', function () { var count = 0; var cursor = db.collection('t1').aggregate(); cursor.match({ a: 1 }); - cursor.each(function (err, doc) { - expect(err).to.not.exist; - if (doc) count = count + 1; - if (doc == null) { + cursor.forEach( + () => { + count = count + 1; + }, + err => { + expect(err).to.not.exist; test.equal(3, count); testStream(); } - }); + ); }; // diff --git a/test/functional/cursor.test.js b/test/functional/cursor.test.js index b031b7f0db..ceb50e1d56 100644 --- a/test/functional/cursor.test.js +++ b/test/functional/cursor.test.js @@ -9,6 +9,7 @@ const { Writable } = require('stream'); const { ReadPreference } = require('../../src/read_preference'); const { ServerType } = require('../../src/sdam/common'); const { formatSort } = require('../../src/sort'); +const { FindCursor } = require('../../src/cursor/find_cursor'); describe('Cursor', function () { before(function () { @@ -51,14 +52,13 @@ describe('Cursor', function () { expect(err).to.not.exist; // Should fail if called again (cursor should be closed) - cursor.each((err, item) => { - expect(err).to.not.exist; - - // Let's close the db - if (!item) { + cursor.forEach( + () => {}, + err => { + expect(err).to.not.exist; done(); } - }); + ); }); }); }); @@ -230,17 +230,18 @@ describe('Cursor', function () { expect(err).to.not.exist; test.equal(10, count); - cursor.each((err, item) => { - expect(err).to.not.exist; - if (item == null) { - cursor.count(function (err, count2) { + cursor.forEach( + () => {}, + err => { + expect(err).to.not.exist; + cursor.count((err, count2) => { expect(err).to.not.exist; - test.equal(10, count2); - test.equal(count, count2); + expect(count2).to.equal(10); + expect(count2).to.equal(count); done(); }); } - }); + ); }); }); }); @@ -280,7 +281,7 @@ describe('Cursor', function () { const db = client.db(configuration.db); const cursor = db.collection('countTEST').find({ qty: { $gt: 4 } }); - cursor.count(true, { readPreference: ReadPreference.SECONDARY }, err => { + cursor.count({ readPreference: ReadPreference.SECONDARY }, err => { expect(err).to.not.exist; const selectedServerAddress = bag[0].address.replace('127.0.0.1', 'localhost'); @@ -349,17 +350,18 @@ describe('Cursor', function () { expect(err).to.not.exist; test.equal(10, count); - cursor.each((err, item) => { - expect(err).to.not.exist; - if (item == null) { - cursor.count(function (err, count2) { + cursor.forEach( + () => {}, + err => { + expect(err).to.not.exist; + cursor.count((err, count2) => { expect(err).to.not.exist; - test.equal(10, count2); - test.equal(count, count2); + expect(count2).to.equal(10); + expect(count2).to.equal(count); done(); }); } - }); + ); }); }); }); @@ -376,106 +378,6 @@ describe('Cursor', function () { } }); - it('shouldCorrectlyExecuteSortOnCursor', { - // Add a tag that our runner can trigger on - // in this case we are setting that node needs to be higher than 0.10.X to run - metadata: { - requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } - }, - - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); - client.connect((err, client) => { - expect(err).to.not.exist; - this.defer(() => client.close()); - - const db = client.db(configuration.db); - db.createCollection('test_sort', (err, collection) => { - expect(err).to.not.exist; - function insert(callback) { - var total = 10; - - for (var i = 0; i < 10; i++) { - collection.insert({ x: i }, configuration.writeConcernMax(), e => { - expect(e).to.not.exist; - total = total - 1; - if (total === 0) callback(); - }); - } - } - - function f() { - var number_of_functions = 7; - var finished = function (cursor) { - number_of_functions = number_of_functions - 1; - if (number_of_functions === 0) { - cursor.close(done); - } else { - cursor.close(); - } - }; - - var cursor = collection.find().sort(['a', 1]); - test.deepEqual({ a: 1 }, cursor.sortValue); - finished(cursor); - - cursor = collection.find().sort('a', 1); - test.deepEqual({ a: 1 }, cursor.sortValue); - finished(cursor); - - cursor = collection.find().sort('a', -1); - test.deepEqual({ a: -1 }, cursor.sortValue); - finished(cursor); - - cursor = collection.find().sort('a', 'asc'); - test.deepEqual({ a: 1 }, cursor.sortValue); - finished(cursor); - - cursor = collection.find().sort('a', 1).sort('a', -1); - test.deepEqual({ a: -1 }, cursor.sortValue); - finished(cursor); - - cursor = collection.find(); - cursor.next(err => { - expect(err).to.not.exist; - try { - cursor.sort(['a']); - } catch (err) { - test.equal('Cursor is closed', err.message); - finished(cursor); - } - }); - - cursor = collection.find(); - try { - cursor.sort('a', 25); - } catch (err) { - test.equal('Invalid sort direction: 25', err.message); - } - cursor.next(() => { - finished(cursor); - }); - - cursor = collection.find(); - try { - cursor.sort(25); - } catch (err) { - test.equal('Invalid sort format: 25', err.message); - } - cursor.next(() => { - finished(cursor); - }); - } - - insert(function () { - f(); - }); - }); - }); - } - }); - it('shouldThrowErrorOnEachWhenMissingCallback', { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run @@ -509,7 +411,7 @@ describe('Cursor', function () { const cursor = collection.find(); test.throws(function () { - cursor.each(); + cursor.forEach(); }); done(); @@ -734,12 +636,9 @@ describe('Cursor', function () { cursor.next(err => { expect(err).to.not.exist; - - try { + expect(() => { cursor.limit(1); - } catch (err) { - test.equal('Cursor is closed', err.message); - } + }).to.throw(/not extensible/); done(); }); @@ -749,7 +648,8 @@ describe('Cursor', function () { } }); - it('shouldCorrectlyReturnErrorsOnIllegalLimitValuesIsClosedWithinClose', { + // NOTE: who cares what you set when the cursor is closed? + it.skip('shouldCorrectlyReturnErrorsOnIllegalLimitValuesIsClosedWithinClose', { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run metadata: { @@ -771,15 +671,11 @@ describe('Cursor', function () { expect(err).to.not.exist; const cursor = collection.find(); - - cursor.close((err, cursor) => { + cursor.close(err => { expect(err).to.not.exist; - try { + expect(() => { cursor.limit(1); - test.ok(false); - } catch (err) { - test.equal('Cursor is closed', err.message); - } + }).to.throw(/not extensible/); done(); }); @@ -890,20 +786,19 @@ describe('Cursor', function () { cursor.next(err => { expect(err).to.not.exist; - try { - cursor.skip(1); - } catch (err) { - test.equal('Cursor is closed', err.message); - } + // NOTE: who cares what you set when closed, if not initialized + // expect(() => { + // cursor.skip(1); + // }).to.throw(/not extensible/); const cursor2 = collection.find(); cursor2.close(err => { expect(err).to.not.exist; - try { - cursor2.skip(1); - } catch (err) { - test.equal('Cursor is closed', err.message); - } + + // NOTE: who cares what you set when closed, if not initialized + // expect(() => { + // cursor2.skip(1); + // }).to.throw(/not extensible/); done(); }); @@ -949,22 +844,19 @@ describe('Cursor', function () { cursor.next(err => { expect(err).to.not.exist; - try { - cursor.batchSize(1); - test.ok(false); - } catch (err) { - test.equal('Cursor is closed', err.message); - } + // NOTE: who cares what you set when closed, if not initialized + // expect(() => { + // cursor.batchSize(1); + // }).to.throw(/not extensible/); const cursor2 = collection.find(); cursor2.close(err => { expect(err).to.not.exist; - try { - cursor2.batchSize(1); - test.ok(false); - } catch (err) { - test.equal('Cursor is closed', err.message); - } + + // NOTE: who cares what you set when closed, if not initialized + // expect(() => { + // cursor2.batchSize(1); + // }).to.throw(/not extensible/); done(); }); @@ -1404,7 +1296,8 @@ describe('Cursor', function () { db.createCollection('test_close_no_query_sent', (err, collection) => { expect(err).to.not.exist; - collection.find().close((err, cursor) => { + const cursor = collection.find(); + cursor.close(err => { expect(err).to.not.exist; test.equal(true, cursor.isClosed()); done(); @@ -1451,11 +1344,12 @@ describe('Cursor', function () { }); var total = 0; - collection.find({}, {}).each((err, item) => { - expect(err).to.not.exist; - if (item != null) { + collection.find({}, {}).forEach( + item => { total = total + item.a; - } else { + }, + err => { + expect(err).to.not.exist; test.equal(499500, total); collection.count((err, count) => { @@ -1468,11 +1362,12 @@ describe('Cursor', function () { test.equal(COUNT, count); var total2 = 0; - collection.find().each((err, item) => { - expect(err).to.not.exist; - if (item != null) { + collection.find().forEach( + item => { total2 = total2 + item.a; - } else { + }, + err => { + expect(err).to.not.exist; test.equal(499500, total2); collection.count((err, count) => { expect(err).to.not.exist; @@ -1481,10 +1376,10 @@ describe('Cursor', function () { done(); }); } - }); + ); }); } - }); + ); } insert(function () { @@ -1531,11 +1426,12 @@ describe('Cursor', function () { }); var total = 0; - collection.find().each((err, item) => { - expect(err).to.not.exist; - if (item != null) { - total = total + item.a; - } else { + collection.find().forEach( + doc => { + total = total + doc.a; + }, + err => { + expect(err).to.not.exist; test.equal(499500, total); collection.count((err, count) => { @@ -1548,23 +1444,25 @@ describe('Cursor', function () { test.equal(1000, count); var total2 = 0; - collection.find().each((err, item) => { - expect(err).to.not.exist; - if (item != null) { - total2 = total2 + item.a; - } else { - test.equal(499500, total2); + collection.find().forEach( + doc => { + total2 = total2 + doc.a; + }, + err => { + expect(err).to.not.exist; + expect(total2).to.equal(499500); + collection.count((err, count) => { expect(err).to.not.exist; - test.equal(1000, count); - test.equal(total, total2); + expect(count).to.equal(1000); + expect(total2).to.equal(total); done(); }); } - }); + ); }); } - }); + ); } insert(function () { @@ -1600,7 +1498,7 @@ describe('Cursor', function () { cursor.next(err => { expect(err).to.not.exist; - cursor.close(function (err, cursor) { + cursor.close(err => { expect(err).to.not.exist; test.equal(true, cursor.isClosed()); done(); @@ -1720,19 +1618,20 @@ describe('Cursor', function () { cursor.count(err => { expect(err).to.not.exist; // Ensure each returns all documents - cursor.each((err, item) => { - expect(err).to.not.exist; - if (item != null) { + cursor.forEach( + () => { total++; - } else { - cursor.count(function (err, c) { + }, + err => { + expect(err).to.not.exist; + cursor.count((err, c) => { expect(err).to.not.exist; - test.equal(1000, c); - test.equal(1000, total); + expect(c).to.equal(1000); + expect(total).to.equal(1000); done(); }); } - }); + ); }); }); }); @@ -1849,7 +1748,7 @@ describe('Cursor', function () { expect(err).to.not.exist; // insert all docs - collection.insert(docs, configuration.writeConcernMax(), err => { + collection.insertMany(docs, configuration.writeConcernMax(), err => { expect(err).to.not.exist; const cursor = collection.find(); @@ -1928,13 +1827,13 @@ describe('Cursor', function () { function testDone(err) { ++finished; - setTimeout(function () { + if (finished === 2) { test.strictEqual(undefined, err); test.strictEqual(5, i); - test.strictEqual(1, finished); + test.strictEqual(2, finished); test.strictEqual(true, cursor.isClosed()); done(); - }, 150); + } } }); }); @@ -2029,24 +1928,13 @@ describe('Cursor', function () { }); // insert all docs - collection.insert(docs, configuration.writeConcernMax(), err => { + collection.insertMany(docs, configuration.writeConcernMax(), err => { expect(err).to.not.exist; - var filename = '/tmp/_nodemongodbnative_stream_out.txt', - out = fs.createWriteStream(filename); - - // hack so we don't need to create a stream filter just to - // stringify the objects (otherwise the created file would - // just contain a bunch of [object Object]) - // var toString = Object.prototype.toString; - // Object.prototype.toString = function () { - // return JSON.stringify(this); - // } - - var stream = collection.find().stream({ - transform: function (doc) { - return JSON.stringify(doc); - } + const filename = '/tmp/_nodemongodbnative_stream_out.txt'; + const out = fs.createWriteStream(filename); + const stream = collection.find().stream({ + transform: doc => JSON.stringify(doc) }); stream.pipe(out); @@ -2090,61 +1978,43 @@ describe('Cursor', function () { this.defer(() => client.close()); const db = client.db(configuration.db); - var options = { capped: true, size: 10000000 }; + const options = { capped: true, size: 10000000 }; db.createCollection('test_if_dead_tailable_cursors_close', options, function ( err, collection ) { expect(err).to.not.exist; - var closeCount = 0; - var errorOccurred = false; + let closeCount = 0; + const docs = Array.from({ length: 100 }).map(() => ({ a: 1 })); + collection.insertMany(docs, { w: 'majority', wtimeout: 5000 }, err => { + expect(err).to.not.exist; - var count = 100; - // Just hammer the server - for (var i = 0; i < 100; i++) { - collection.insert({ id: i }, { w: 'majority', wtimeout: 5000 }, err => { - expect(err).to.not.exist; - count = count - 1; + const cursor = collection.find({}, { tailable: true, awaitData: true }); + const stream = cursor.stream(); - if (count === 0) { - const cursor = collection.find({}, { tailable: true, awaitData: true }); - const stream = cursor.stream(); - // let index = 0; - stream.resume(); + stream.resume(); - stream.on('error', err => { - expect(err).to.exist; - errorOccurred = true; - }); + var validator = () => { + closeCount++; + if (closeCount === 2) { + done(); + } + }; - var validator = () => { - closeCount++; - if (closeCount === 2) { - expect(errorOccurred).to.equal(true); - done(); - } - }; + // we validate that the stream "ends" either cleanly or with an error + stream.on('end', validator); + stream.on('error', validator); - stream.on('end', validator); - cursor.on('close', validator); + cursor.on('close', validator); - // Just hammer the server - for (var i = 0; i < 100; i++) { - const id = i; - process.nextTick(function () { - collection.insert({ id }, err => { - expect(err).to.not.exist; + const docs = Array.from({ length: 100 }).map(() => ({ a: 1 })); + collection.insertMany(docs, err => { + expect(err).to.not.exist; - if (id === 99) { - setTimeout(() => client.close()); - } - }); - }); - } - } + setTimeout(() => client.close()); }); - } + }); }); }); } @@ -2182,17 +2052,14 @@ describe('Cursor', function () { this.defer(() => cursor.close()); // Execute each - cursor.each((err, result) => { - if (result) { - cursor.kill(); - } - - if (err != null) { + cursor.forEach( + () => cursor.close(), + () => { // Even though cursor is exhausted, should not close session // unless cursor is manually closed, due to awaitData / tailable done(); } - }); + ); }); } ); @@ -2200,94 +2067,37 @@ describe('Cursor', function () { } }); - it('shouldAwaitDataWithDocumentsAvailable', { - // Add a tag that our runner can trigger on - // in this case we are setting that node needs to be higher than 0.10.X to run - metadata: { - requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } - }, - - test: function (done) { - // http://www.mongodb.org/display/DOCS/Tailable+Cursors - - const configuration = this.configuration; - const client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); - client.connect((err, client) => { - expect(err).to.not.exist; - this.defer(() => client.close()); + it('shouldAwaitDataWithDocumentsAvailable', function (done) { + // http://www.mongodb.org/display/DOCS/Tailable+Cursors - const db = client.db(configuration.db); - const options = { capped: true, size: 8 }; - db.createCollection('should_await_data_no_docs', options, (err, collection) => { - expect(err).to.not.exist; - - // Create cursor with awaitData, and timeout after the period specified - const cursor = collection.find({}, { tailable: true, awaitData: true }); - this.defer(() => cursor.close()); - - const rewind = cursor.rewind; - let called = false; - cursor.rewind = function () { - called = true; - }; - - cursor.each(err => { - if (err != null) { - test.ok(called); - cursor.rewind = rewind; - done(); - } - }); - }); - }); - } - }); - - it('shouldAwaitDataUsingCursorFlag', { - // Add a tag that our runner can trigger on - // in this case we are setting that node needs to be higher than 0.10.X to run - metadata: { - requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } - }, - - test: function (done) { - // http://www.mongodb.org/display/DOCS/Tailable+Cursors + const configuration = this.configuration; + const client = configuration.newClient({ maxPoolSize: 1 }); + client.connect((err, client) => { + expect(err).to.not.exist; + this.defer(() => client.close()); - const configuration = this.configuration; - const client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); - client.connect((err, client) => { + const db = client.db(configuration.db); + const options = { capped: true, size: 8 }; + db.createCollection('should_await_data_no_docs', options, (err, collection) => { expect(err).to.not.exist; - this.defer(() => client.close()); - const db = client.db(configuration.db); - const options = { capped: true, size: 8 }; - db.createCollection('should_await_data_cursor_flag', options, (err, collection) => { - expect(err).to.not.exist; + // Create cursor with awaitData, and timeout after the period specified + const cursor = collection.find({}, { tailable: true, awaitData: true }); + this.defer(() => cursor.close()); - collection.insert({ a: 1 }, configuration.writeConcernMax(), err => { + cursor.forEach( + () => {}, + err => { expect(err).to.not.exist; - // Create cursor with awaitData, and timeout after the period specified - const cursor = collection.find({}, {}); - this.defer(() => cursor.close()); - - cursor.addCursorFlag('tailable', true); - cursor.addCursorFlag('awaitData', true); - cursor.each(err => { - if (err != null) { - // Even though cursor is exhausted, should not close session - // unless cursor is manually closed, due to awaitData / tailable - done(); - } else { - cursor.kill(); - } - }); - }); - }); + done(); + } + ); }); - } + }); }); - it('Should correctly retry tailable cursor connection', { + // NOTE: should we continue to let users explicitly `kill` a cursor? + it.skip('Should correctly retry tailable cursor connection', { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run metadata: { @@ -2313,14 +2123,13 @@ describe('Cursor', function () { // Create cursor with awaitData, and timeout after the period specified var cursor = collection.find({}, { tailable: true, awaitData: true }); - cursor.each(err => { - if (err != null) { + cursor.forEach( + () => cursor.kill(), + () => { // kill cursor b/c cursor is tailable / awaitable cursor.close(done); - } else { - cursor.kill(); } - }); + ); }); }); }); @@ -2656,15 +2465,16 @@ describe('Cursor', function () { totalI = totalI + d.length; if (left === 0) { - collection.find({}).each((err, item) => { - expect(err).to.not.exist; - if (item == null) { - test.equal(30000, total); - done(); - } else { + collection.find({}).forEach( + () => { total++; + }, + err => { + expect(err).to.not.exist; + expect(total).to.equal(30000); + done(); } - }); + ); } }); } @@ -2767,7 +2577,7 @@ describe('Cursor', function () { .find({}, { OrderNumber: 1 }) .skip(10) .limit(10) - .count(true, (err, count) => { + .count((err, count) => { expect(err).to.not.exist; test.equal(10, count); done(); @@ -2801,14 +2611,17 @@ describe('Cursor', function () { expect(err).to.not.exist; const cursor = collection.find({}, { tailable: true }); - cursor.each(err => { - test.ok(err instanceof Error); - test.ok(typeof err.code === 'number'); + cursor.forEach( + () => {}, + err => { + test.ok(err instanceof Error); + test.ok(typeof err.code === 'number'); - // Close cursor b/c we did not exhaust cursor - cursor.close(); - done(); - }); + // Close cursor b/c we did not exhaust cursor + cursor.close(); + done(); + } + ); }); }); } @@ -2960,17 +2773,19 @@ describe('Cursor', function () { expect(err).to.not.exist; var finished = false; - collection.find({}).each(function (err, doc) { - expect(err).to.not.exist; - - if (doc) { + collection.find({}).forEach( + doc => { + expect(doc).to.exist; test.equal(finished, false); finished = true; done(); return false; + }, + err => { + expect(err).to.not.exist; } - }); + ); }); }); }); @@ -3108,7 +2923,7 @@ describe('Cursor', function () { var cursor = collection.find({}); cursor.limit(100); cursor.skip(10); - cursor.count(true, { maxTimeMS: 1000 }, err => { + cursor.count({ maxTimeMS: 1000 }, err => { expect(err).to.not.exist; // Create a cursor for the content @@ -3116,9 +2931,9 @@ describe('Cursor', function () { cursor.limit(100); cursor.skip(10); cursor.maxTimeMS(100); + cursor.count(err => { expect(err).to.not.exist; - done(); }); }); @@ -3215,8 +3030,8 @@ describe('Cursor', function () { test.equal(10, docs.length); // Ensure all docs where mapped - docs.forEach(function (x) { - test.equal(1, x.a); + docs.forEach(doc => { + expect(doc).property('a').to.equal(1); }); done(); @@ -3310,15 +3125,15 @@ describe('Cursor', function () { .batchSize(5) .limit(10); - cursor.each(function (err, doc) { - expect(err).to.not.exist; - - if (doc) { + cursor.forEach( + doc => { test.equal(1, doc.a); - } else { + }, + err => { + expect(err).to.not.exist; done(); } - }); + ); }); }); } @@ -3365,7 +3180,7 @@ describe('Cursor', function () { .limit(10); cursor.forEach( - function (doc) { + doc => { test.equal(4, doc.a); }, err => { @@ -3416,8 +3231,8 @@ describe('Cursor', function () { .limit(10); cursor.forEach( - function (doc) { - test.equal(1, doc.a); + doc => { + expect(doc).property('a').to.equal(1); }, err => { expect(err).to.not.exist; @@ -3477,7 +3292,7 @@ describe('Cursor', function () { test: function (done) { const configuration = this.configuration; - const client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); + const client = configuration.newClient(); client.connect((err, client) => { expect(err).to.not.exist; this.defer(() => client.close()); @@ -3493,7 +3308,6 @@ describe('Cursor', function () { collection.insert({ a: 1 }, configuration.writeConcernMax(), err => { expect(err).to.not.exist; - var s = new Date(); // Create cursor with awaitData, and timeout after the period specified var cursor = collection .find({}) @@ -3501,18 +3315,16 @@ describe('Cursor', function () { .addCursorFlag('awaitData', true) .maxAwaitTimeMS(500); - cursor.each(function (err, result) { - if (result) { - setTimeout(function () { - cursor.kill(); - }, 300); - } else { + const s = new Date(); + cursor.forEach( + () => { + setTimeout(() => cursor.close(), 300); + }, + () => { test.ok(new Date().getTime() - s.getTime() >= 500); - - // TODO: forced because the cursor is still open/active - client.close(true, done); + done(); } - }); + ); }); }); }); @@ -3742,7 +3554,7 @@ describe('Cursor', function () { .limit(5) .skip(5) .hint({ project: 1 }) - .count(true, err => { + .count(err => { expect(err).to.not.exist; test.equal(1, started.length); if (started[0].command.readConcern) @@ -3812,7 +3624,8 @@ describe('Cursor', function () { } }); - it('Should properly kill a cursor', { + // NOTE: should we allow users to explicitly `kill` a cursor anymore? + it.skip('Should properly kill a cursor', { metadata: { requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'], @@ -3912,15 +3725,13 @@ describe('Cursor', function () { this.defer(() => client.close()); const db = client.db(configuration.db); - var findCommand = { - find: 'integration_tests.has_next_error_callback', - limit: 0, - skip: 0, - query: {}, - slaveOk: false - }; + const cursor = new FindCursor( + db.s.topology, + db.s.namespace, + {}, + { limit: 0, skip: 0, slaveOk: false, readPreference: 42 } + ); - var cursor = db.s.topology.cursor(db.namespace, findCommand, { readPreference: 42 }); cursor.hasNext(err => { test.ok(err !== null); test.equal(err.message, 'readPreference must be a ReadPreference instance'); @@ -4286,7 +4097,6 @@ describe('Cursor', function () { db.collection('test_sort_dos', (err, collection) => { expect(err).to.not.exist; const cursor = collection.find({}, { sort: input }); - expect(cursor.sortValue).to.deep.equal(output); cursor.next(err => { expect(err).to.not.exist; expect(events[0].command.sort).to.deep.equal(output); @@ -4301,7 +4111,6 @@ describe('Cursor', function () { db.collection('test_sort_dos', (err, collection) => { expect(err).to.not.exist; const cursor = collection.find({}).sort(input); - expect(cursor.sortValue).to.deep.equal(output); cursor.next(err => { expect(err).to.not.exist; expect(events[0].command.sort).to.deep.equal(output); diff --git a/test/functional/cursorstream.test.js b/test/functional/cursorstream.test.js index f99e1ba966..211d6fa8c3 100644 --- a/test/functional/cursorstream.test.js +++ b/test/functional/cursorstream.test.js @@ -281,28 +281,21 @@ describe('Cursor Streams', function () { maxPoolSize: 1 }); - client.connect(function (err, client) { - var db = client.db(self.configuration.db); - var cursor = db.collection('myCollection').find({ + client.connect((err, client) => { + const db = client.db(self.configuration.db); + const cursor = db.collection('myCollection').find({ timestamp: { $ltx: '1111' } // Error in query. }); - var error, streamIsClosed; + let error; const stream = cursor.stream(); - - stream.on('error', function (err) { - error = err; - }); - + stream.on('error', err => (error = err)); cursor.on('close', function () { - expect(error).to.exist; - streamIsClosed = true; - }); - - stream.on('end', function () { - expect(error).to.exist; - expect(streamIsClosed).to.be.true; - client.close(done); + // NOTE: use `setImmediate` here because the stream implementation uses `nextTick` to emit the error + setImmediate(() => { + expect(error).to.exist; + client.close(done); + }); }); stream.pipe(process.stdout); diff --git a/test/functional/find.test.js b/test/functional/find.test.js index 3a275b9439..4e653ade03 100644 --- a/test/functional/find.test.js +++ b/test/functional/find.test.js @@ -1129,25 +1129,6 @@ describe('Find', function () { } }); - it('Should correctly pass timeout options to cursor noCursorTimeout', { - metadata: { - requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } - }, - - test: function (done) { - var configuration = this.configuration; - var client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); - client.connect(function (err, client) { - var db = client.db(configuration.db); - db.createCollection('timeoutFalse', function (err, collection) { - const cursor = collection.find({}, {}); - test.ok(!cursor.cmd.noCursorTimeout); - client.close(done); - }); - }); - } - }); - it( 'should support a timeout option for find operations', withMonitoredClient(['find'], function (client, events, done) { @@ -1716,14 +1697,17 @@ describe('Find', function () { var cursor = collection.find({}, {}); cursor.count(function (err) { expect(err).to.not.exist; - cursor.each(function (err, obj) { - if (obj == null) { + cursor.forEach( + doc => { + expect(doc).to.exist; + numberOfSteps = numberOfSteps + 1; + }, + err => { + expect(err).to.not.exist; test.equal(500, numberOfSteps); p_client.close(done); - } else { - numberOfSteps = numberOfSteps + 1; } - }); + ); }); }); }); @@ -2160,11 +2144,15 @@ describe('Find', function () { // Create a collection we want to drop later db.collection('noresultAvailableForEachToIterate', function (err, collection) { // Perform a simple find and return all the documents - collection.find({}).each(function (err, item) { - expect(item).to.not.exist; - - client.close(done); - }); + collection.find({}).forEach( + doc => { + expect(doc).to.not.exist; + }, + err => { + expect(err).to.not.exist; + client.close(done); + } + ); }); }); } @@ -2318,52 +2306,6 @@ describe('Find', function () { } }); - it('Should simulate closed cursor', { - // Add a tag that our runner can trigger on - // in this case we are setting that node needs to be higher than 0.10.X to run - metadata: { requires: { mongodb: '>2.5.5', topology: ['single', 'replicaset'] } }, - - test: function (done) { - var configuration = this.configuration; - var client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); - client.connect((err, client) => { - expect(err).to.not.exist; - this.defer(() => client.close()); - - const db = client.db(configuration.db); - const docs = []; - for (let i = 0; i < 1000; i++) { - docs.push({ a: i }); - } - - // Get the collection - const collection = db.collection('simulate_closed_cursor'); - // Insert 1000 documents in a batch - collection.insert(docs, err => { - expect(err).to.not.exist; - - // Get the cursor - var cursor = collection.find({}).batchSize(2); - this.defer(() => cursor.close()); - - // Get next document - cursor.next((err, doc) => { - expect(err).to.not.exist; - test.ok(doc != null); - - // Mess with state forcing a call to isDead on the cursor - cursor.s.state = 2; - - cursor.next(err => { - test.ok(err !== null); - done(); - }); - }); - }); - }); - } - }); - /** * Find and modify should allow for a write Concern without failing */ diff --git a/test/functional/generator_based.test.js b/test/functional/generator_based.test.js deleted file mode 100644 index 7affc823ee..0000000000 --- a/test/functional/generator_based.test.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; -var test = require('./shared').assert; -var setupDatabase = require('./shared').setupDatabase; - -describe('Generators', function () { - before(function () { - return setupDatabase(this.configuration); - }); - - it('should maintain batch size between calls to receive new batches', { - metadata: { - // MongoDb must be > 2.6.0 as aggregate did not return a cursor before this version - requires: { generators: true, topology: 'single', node: '>6.0.0', mongodb: '>=2.6.0' } - }, - - test: function (done) { - var co = require('co'); - var configuration = this.configuration; - - co(function* () { - var instance = configuration.newClient({ w: 1 }, { maxPoolSize: 1 }); - var client = yield instance.connect(); - var db = client.db(configuration.db); - - var docs = [{ a: 1 }, { a: 1 }, { a: 1 }, { a: 1 }, { a: 1 }, { a: 1 }]; - var collection = db.collection('batchSizeContinue'); - yield collection.insertMany(docs, { w: 1 }); - var cursor = collection.aggregate([{ $match: { a: 1 } }, { $limit: 6 }], { - cursor: { batchSize: 2 } - }); - - var count = 0; - while (yield cursor.hasNext()) { - var data = yield cursor.next(); - test.equal(data.a, 1); - - // ensure batch size is as specified - test.equal(cursor.documents.length, 2); - count++; - } - - test.equal(count, 6); - client.close(done); - }).catch(err => { - done(err); - }); - } - }); -}); diff --git a/test/functional/operation_example.test.js b/test/functional/operation_example.test.js index 1bc2930dc2..e5b84152a1 100644 --- a/test/functional/operation_example.test.js +++ b/test/functional/operation_example.test.js @@ -442,13 +442,13 @@ describe('Operation Examples', function () { ); // Get all the aggregation results - cursor.each(function (err, docs) { - expect(err).to.not.exist; - - if (docs == null) { + cursor.forEach( + () => {}, + err => { + expect(err).to.not.exist; client.close(done); } - }); + ); }); }); // END @@ -1709,7 +1709,7 @@ describe('Operation Examples', function () { var client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); /* eslint-disable */ - client.connect(function(err, client) { + client.connect(function (err, client) { // LINE var MongoClient = require('mongodb').MongoClient, // LINE test = require('assert'); // LINE const client = new MongoClient('mongodb://localhost:27017/test'); @@ -1725,31 +1725,31 @@ describe('Operation Examples', function () { var collection = db.collection('test_map_reduce_functions'); // Insert some documents to perform map reduce over - collection.insertMany([{ user_id: 1 }, { user_id: 2 }], { w: 1 }, function(err, r) { + collection.insertMany([{ user_id: 1 }, { user_id: 2 }], { w: 1 }, function (err, r) { test.ok(r); expect(err).to.not.exist; // Map function - var map = function() { + var map = function () { emit(this.user_id, 1); }; // Reduce function - var reduce = function(k, vals) { + var reduce = function (k, vals) { return 1; }; // Perform the map reduce - collection.mapReduce(map, reduce, { out: { replace: 'tempCollection' } }, function( + collection.mapReduce(map, reduce, { out: { replace: 'tempCollection' } }, function ( err, collection ) { expect(err).to.not.exist; // Mapreduce returns the temporary collection with the results - collection.findOne({ _id: 1 }, function(err, result) { + collection.findOne({ _id: 1 }, function (err, result) { test.equal(1, result.value); - collection.findOne({ _id: 2 }, function(err, result) { + collection.findOne({ _id: 2 }, function (err, result) { test.equal(1, result.value); client.close(done); @@ -1809,7 +1809,7 @@ describe('Operation Examples', function () { // Reduce function // eslint-disable-next-line - var reduce = function(k, vals) { + var reduce = function (k, vals) { return 1; }; @@ -2758,7 +2758,7 @@ describe('Operation Examples', function () { // Attemp to rename a collection to a number try { - collection1.rename(5, function(err, collection) {}); // eslint-disable-line + collection1.rename(5, function (err, collection) {}); // eslint-disable-line } catch (err) { test.ok(err instanceof Error); test.equal('collection name must be a String', err.message); @@ -2766,7 +2766,7 @@ describe('Operation Examples', function () { // Attemp to rename a collection to an empty string try { - collection1.rename('', function(err, collection) {}); // eslint-disable-line + collection1.rename('', function (err, collection) {}); // eslint-disable-line } catch (err) { test.ok(err instanceof Error); test.equal('collection names cannot be empty', err.message); @@ -2774,7 +2774,7 @@ describe('Operation Examples', function () { // Attemp to rename a collection to an illegal name including the character $ try { - collection1.rename('te$t', function(err, collection) {}); // eslint-disable-line + collection1.rename('te$t', function (err, collection) {}); // eslint-disable-line } catch (err) { test.ok(err instanceof Error); test.equal("collection names must not contain '$'", err.message); @@ -2782,7 +2782,7 @@ describe('Operation Examples', function () { // Attemp to rename a collection to an illegal name starting with the character . try { - collection1.rename('.test', function(err, collection) {}); // eslint-disable-line + collection1.rename('.test', function (err, collection) {}); // eslint-disable-line } catch (err) { test.ok(err instanceof Error); test.equal("collection names must not start or end with '.'", err.message); @@ -2790,7 +2790,7 @@ describe('Operation Examples', function () { // Attemp to rename a collection to an illegal name ending with the character . try { - collection1.rename('test.', function(err, collection) {}); // eslint-disable-line + collection1.rename('test.', function (err, collection) {}); // eslint-disable-line } catch (err) { test.ok(err instanceof Error); test.equal("collection names must not start or end with '.'", err.message); @@ -2798,7 +2798,7 @@ describe('Operation Examples', function () { // Attemp to rename a collection to an illegal name with an empty middle name try { - collection1.rename('tes..t', function(err, collection) {}); // eslint-disable-line + collection1.rename('tes..t', function (err, collection) {}); // eslint-disable-line } catch (err) { test.equal('collection names cannot be empty', err.message); } @@ -4823,19 +4823,21 @@ describe('Operation Examples', function () { // Grab a cursor var cursor = collection.find(); // Execute the each command, triggers for each document - cursor.each(function (err, item) { - // If the item is null then the cursor is exhausted/empty and closed - if (item == null) { + cursor.forEach( + () => {}, + err => { + expect(err).to.not.exist; + // Show that the cursor is closed - cursor.toArray(function (err, items) { - test.ok(items); + cursor.toArray((err, docs) => { expect(err).to.not.exist; + expect(docs).to.exist; // Let's close the db client.close(done); }); } - }); + ); }); }); // END @@ -4906,7 +4908,8 @@ describe('Operation Examples', function () { * @example-class Cursor * @example-method rewind */ - it('Should correctly rewind and restart cursor', { + // NOTE: unclear whether we should continue to support `rewind` + it.skip('Should correctly rewind and restart cursor', { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run metadata: { @@ -5500,8 +5503,7 @@ describe('Operation Examples', function () { expect(err).to.not.exist; // Close the cursor, this is the same as reseting the query - cursor.close(function (err, result) { - test.ok(result); + cursor.close(function (err) { expect(err).to.not.exist; test.equal(true, cursor.isClosed()); @@ -5565,8 +5567,7 @@ describe('Operation Examples', function () { expect(err).to.not.exist; // Close the cursor, this is the same as reseting the query - cursor.close(function (err, result) { - test.ok(result); + cursor.close(function (err) { expect(err).to.not.exist; client.close(done); @@ -6725,11 +6726,11 @@ describe('Operation Examples', function () { stream.on('data', function () { total = total + 1; if (total === 1000) { - cursor.kill(); + cursor.close(); } }); - stream.on('end', function () { + cursor.on('close', function () { client.close(done); }); }); diff --git a/test/functional/operation_generators_example.test.js b/test/functional/operation_generators_example.test.js index 6dda347c57..4474a0e1c7 100644 --- a/test/functional/operation_generators_example.test.js +++ b/test/functional/operation_generators_example.test.js @@ -4624,11 +4624,11 @@ describe('Operation (Generators)', function () { total = total + 1; if (total === 1000) { - cursor.kill(); + cursor.close(); } }); - stream.on('end', function () { + cursor.on('close', function () { // TODO: forced because the cursor is still open/active client.close(true, err => { if (err) return reject(err); diff --git a/test/functional/operation_promises_example.test.js b/test/functional/operation_promises_example.test.js index 4455a42163..112c5ad4ad 100644 --- a/test/functional/operation_promises_example.test.js +++ b/test/functional/operation_promises_example.test.js @@ -533,7 +533,7 @@ describe('Operation (Promises)', function () { test: function () { var configuration = this.configuration; - var client = configuration.newClient({ w: 0 }, { maxPoolSize: 1 }); + var client = configuration.newClient({ maxPoolSize: 1 }); return client.connect().then(function (client) { var db = client.db(configuration.db); @@ -665,7 +665,7 @@ describe('Operation (Promises)', function () { test: function () { var configuration = this.configuration; - var client = configuration.newClient({ w: 0 }, { maxPoolSize: 1 }); + var client = configuration.newClient({ maxPoolSize: 1 }); return client.connect().then(function (client) { var db = client.db(configuration.db); @@ -1208,7 +1208,7 @@ describe('Operation (Promises)', function () { test: function () { var configuration = this.configuration; - var client = configuration.newClient({ w: 0 }, { maxPoolSize: 1 }); + var client = configuration.newClient({ maxPoolSize: 1 }); return client.connect().then(function (client) { var db = client.db(configuration.db); @@ -1508,7 +1508,7 @@ describe('Operation (Promises)', function () { test: function () { var configuration = this.configuration; - var client = configuration.newClient({ w: 0 }, { maxPoolSize: 1 }); + var client = configuration.newClient({ maxPoolSize: 1 }); return client.connect().then(function (client) { var db = client.db(configuration.db); @@ -1581,7 +1581,7 @@ describe('Operation (Promises)', function () { test: function () { var configuration = this.configuration; - var client = configuration.newClient({ w: 0 }, { maxPoolSize: 1 }); + var client = configuration.newClient({ maxPoolSize: 1 }); return client.connect().then(function (client) { var db = client.db(configuration.db); @@ -1953,7 +1953,7 @@ describe('Operation (Promises)', function () { test: function () { var configuration = this.configuration; - var client = configuration.newClient({ w: 0 }, { maxPoolSize: 1 }); + var client = configuration.newClient({ maxPoolSize: 1 }); return client.connect().then(function (client) { var db = client.db(configuration.db); @@ -2375,7 +2375,7 @@ describe('Operation (Promises)', function () { test: function () { var configuration = this.configuration; - var client = configuration.newClient({ w: 0 }, { maxPoolSize: 1 }); + var client = configuration.newClient({ maxPoolSize: 1 }); return client.connect().then(function (client) { var db = client.db(configuration.db); @@ -4955,10 +4955,11 @@ describe('Operation (Promises)', function () { total = total + 1; if (total === 1000) { - cursor.kill(); + cursor.close(); } }); - stream.on('end', function () { + + cursor.on('close', function () { // TODO: forced because the cursor is still open/active client.close(true, done); }); diff --git a/test/functional/promote_values.test.js b/test/functional/promote_values.test.js index fd246754de..c301a1c497 100644 --- a/test/functional/promote_values.test.js +++ b/test/functional/promote_values.test.js @@ -228,7 +228,7 @@ describe('Promote Values', function () { var db = client.db(configuration.db); - db.collection('haystack').insert(docs, function (errInsert) { + db.collection('haystack').insertMany(docs, function (errInsert) { if (errInsert) throw errInsert; // change limit from 102 to 101 and this test passes. // seems to indicate that the promoteValues flag is used for the diff --git a/test/functional/readpreference.test.js b/test/functional/readpreference.test.js index 7e5117a951..1993e29499 100644 --- a/test/functional/readpreference.test.js +++ b/test/functional/readpreference.test.js @@ -440,7 +440,7 @@ describe('ReadPreference', function () { var cursor = db .collection('test', { readPreference: ReadPreference.SECONDARY_PREFERRED }) .listIndexes(); - test.equal(cursor.options.readPreference.mode, 'secondaryPreferred'); + test.equal(cursor.readPreference.mode, 'secondaryPreferred'); client.close(done); }); } diff --git a/test/functional/spec-runner/index.js b/test/functional/spec-runner/index.js index 55cb3961c1..37612743fb 100644 --- a/test/functional/spec-runner/index.js +++ b/test/functional/spec-runner/index.js @@ -412,13 +412,13 @@ function validateExpectations(commandEvents, spec, savedSessionData) { } function normalizeCommandShapes(commands) { - return commands.map(command => + return commands.map(def => JSON.parse( EJSON.stringify( { - command: command.command, - commandName: command.command_name ? command.command_name : command.commandName, - databaseName: command.database_name ? command.database_name : command.databaseName + command: def.command, + commandName: def.command_name || def.commandName || Object.keys(def.command)[0], + databaseName: def.database_name ? def.database_name : def.databaseName }, { relaxed: true } ) diff --git a/test/functional/unicode.test.js b/test/functional/unicode.test.js index cf6c0e98aa..5d1368f4a6 100644 --- a/test/functional/unicode.test.js +++ b/test/functional/unicode.test.js @@ -88,6 +88,7 @@ describe('Unicode', function () { var configuration = this.configuration; var client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); client.connect(function (err, client) { + expect(err).to.not.exist; var db = client.db(configuration.db); db.createCollection('unicode_test_collection', function (err, collection) { var test_strings = ['ouooueauiOUOOUEAUI', 'öüóőúéáűíÖÜÓŐÚÉÁŰÍ', '本荘由利地域に洪水警報']; @@ -97,13 +98,15 @@ describe('Unicode', function () { expect(err).to.not.exist; collection.insert({ id: 2, text: test_strings[2] }, { w: 1 }, function (err) { expect(err).to.not.exist; - collection.find().each(function (err, item) { - if (item != null) { - test.equal(test_strings[item.id], item.text); - } else { + collection.find().forEach( + doc => { + expect(doc).property('text').to.equal(test_strings[doc.id]); + }, + err => { + expect(err).to.not.exist; client.close(done); } - }); + ); }); }); }); diff --git a/test/unit/core/response_test.js.test.js b/test/unit/core/response_test.js.test.js index 113de382e2..98c16b0dc0 100644 --- a/test/unit/core/response_test.js.test.js +++ b/test/unit/core/response_test.js.test.js @@ -5,6 +5,8 @@ const { MongoError } = require('../../../src/error'); const mock = require('mongodb-mock-server'); const { Topology } = require('../../../src/sdam/topology'); const { Long } = require('bson'); +const { MongoDBNamespace } = require('../../../src/utils'); +const { FindCursor } = require('../../../src/cursor/find_cursor'); const test = {}; describe('Response', function () { @@ -48,7 +50,7 @@ describe('Response', function () { client.on('error', done); client.once('connect', () => { - const cursor = client.cursor('test.test', { find: 'test' }); + const cursor = new FindCursor(client, MongoDBNamespace.fromString('test.test'), {}, {}); // Execute next cursor.next(function (err) { diff --git a/test/unit/sessions/collection.test.js b/test/unit/sessions/collection.test.js index 0e87077151..bc4943c0f5 100644 --- a/test/unit/sessions/collection.test.js +++ b/test/unit/sessions/collection.test.js @@ -29,7 +29,7 @@ describe('Sessions', function () { request.reply({ ok: 1, operationTime: insertOperationTime }); } else if (doc.find) { findCommand = doc; - request.reply({ ok: 1 }); + request.reply({ ok: 1, cursor: { id: 0, firstBatch: [] } }); } else if (doc.endSessions) { request.reply({ ok: 1 }); } From 5cf7748f7ad1b02f1c9cc1a0be3e5b29fae1d57a Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Fri, 13 Nov 2020 07:29:19 -0500 Subject: [PATCH 02/14] clamp `readBufferedDocuments` to [0, bufferedCount] --- src/cursor/abstract_cursor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index e5eab2c4fa..0df0eaa176 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -170,7 +170,7 @@ export abstract class AbstractCursor extends EventEmitter { /** Returns current buffered documents */ readBufferedDocuments(number: number): Document[] { - return this[kDocuments].splice(0, number); + return this[kDocuments].splice(0, Math.max(0, Math.min(number, this[kDocuments].length))); } [Symbol.asyncIterator](): AsyncIterator { From c589a3407bb78e746e4e7d638df3296a9cfef2b5 Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Fri, 13 Nov 2020 07:53:08 -0500 Subject: [PATCH 03/14] Document => ResumeToken --- src/change_stream.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/change_stream.ts b/src/change_stream.ts index 80edeaaf4f..c6b4b41967 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -350,7 +350,7 @@ export class ChangeStreamCursor extends AbstractCursor { startAfter: ResumeToken; options: ChangeStreamCursorOptions; - postBatchResumeToken?: Document; + postBatchResumeToken?: ResumeToken; pipeline: Document[]; constructor( From c2bd34361618f13da348af8aecaf30ae33372106 Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Fri, 13 Nov 2020 07:53:20 -0500 Subject: [PATCH 04/14] remove comment about properties belonging --- src/cursor/abstract_cursor.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 0df0eaa176..10fbad2275 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -379,8 +379,6 @@ export abstract class AbstractCursor extends EventEmitter { }); } - // DO THESE PROPERTIES BELONG HERE? - /** * Add a cursor flag to the cursor * From e0efd0919b64f4a78fe4235b52535e8617f3fe6a Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Fri, 13 Nov 2020 08:16:23 -0500 Subject: [PATCH 05/14] add cursor initialization checks --- src/cursor/abstract_cursor.ts | 18 +++++++ src/cursor/aggregation_cursor.ts | 35 +++++++----- src/cursor/find_cursor.ts | 22 ++++++-- test/functional/cursor.test.js | 93 +------------------------------- 4 files changed, 60 insertions(+), 108 deletions(-) diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 10fbad2275..7d4a679127 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -17,6 +17,7 @@ const kTopology = Symbol('topology'); const kSession = Symbol('session'); const kOptions = Symbol('options'); const kTransform = Symbol('transform'); +const kInitialized = Symbol('iniitalized'); const kClosed = Symbol('closed'); const kKilled = Symbol('killed'); @@ -77,6 +78,7 @@ export abstract class AbstractCursor extends EventEmitter { [kDocuments]: Document[]; [kTopology]: Topology; [kTransform]?: (doc: Document) => Document; + [kInitialized]: boolean; [kClosed]: boolean; [kKilled]: boolean; [kOptions]: InternalAbstractCursorOptions; @@ -94,6 +96,7 @@ export abstract class AbstractCursor extends EventEmitter { this[kTopology] = topology; this[kNamespace] = namespace; this[kDocuments] = []; // TODO: https://github.com/microsoft/TypeScript/issues/36230 + this[kInitialized] = false; this[kClosed] = false; this[kKilled] = false; this[kOptions] = { @@ -386,6 +389,7 @@ export abstract class AbstractCursor extends EventEmitter { * @param value - The flag boolean value. */ addCursorFlag(flag: CursorFlag, value: boolean): this { + assertUninitialized(this); if (!CURSOR_FLAGS.includes(flag)) { throw new MongoError(`flag ${flag} is not one of ${CURSOR_FLAGS}`); } @@ -404,6 +408,7 @@ export abstract class AbstractCursor extends EventEmitter { * @param transform - The mapping transformation method. */ map(transform: (doc: Document) => Document): this { + assertUninitialized(this); const oldTransform = this[kTransform]; if (oldTransform) { this[kTransform] = doc => { @@ -422,6 +427,7 @@ export abstract class AbstractCursor extends EventEmitter { * @param readPreference - The new read preference for the cursor. */ setReadPreference(readPreference: ReadPreferenceLike): this { + assertUninitialized(this); if (readPreference instanceof ReadPreference) { this[kOptions].readPreference = readPreference; } else if (typeof readPreference === 'string') { @@ -439,6 +445,7 @@ export abstract class AbstractCursor extends EventEmitter { * @param value - Number of milliseconds to wait before aborting the query. */ maxTimeMS(value: number): this { + assertUninitialized(this); if (typeof value !== 'number') { throw new TypeError('maxTimeMS must be a number'); } @@ -453,6 +460,7 @@ export abstract class AbstractCursor extends EventEmitter { * @param value - The number of documents to return per batch. See {@link https://docs.mongodb.com/manual/reference/command/find/|find command documentation}. */ batchSize(value: number): this { + assertUninitialized(this); if (this[kOptions].tailable) { throw new MongoError('Tailable cursors do not support batchSize'); } @@ -570,6 +578,9 @@ function next(cursor: AbstractCursor, callback: Callback): void } } + // the cursor is now initialized, even if an error occurred or it is dead + cursor[kInitialized] = true; + if (err || cursorIsDead(cursor)) { return cleanupCursor(cursor, () => callback(err, nextDocument(cursor))); } @@ -624,6 +635,13 @@ function cleanupCursor(cursor: AbstractCursor, callback: Callback): void { } } +/** @internal */ +export function assertUninitialized(cursor: AbstractCursor): void { + if (cursor[kInitialized]) { + throw new MongoError('Cursor is already initialized'); + } +} + function makeCursorStream(cursor: AbstractCursor) { const readable = new Readable({ objectMode: true, diff --git a/src/cursor/aggregation_cursor.ts b/src/cursor/aggregation_cursor.ts index be146eb23a..9966845455 100644 --- a/src/cursor/aggregation_cursor.ts +++ b/src/cursor/aggregation_cursor.ts @@ -1,5 +1,5 @@ import { AggregateOperation, AggregateOptions } from '../operations/aggregate'; -import { AbstractCursor } from './abstract_cursor'; +import { AbstractCursor, assertUninitialized } from './abstract_cursor'; import { executeOperation, ExecutionResult } from '../operations/execute_operation'; import type { Document } from '../bson'; import type { Sort } from '../sort'; @@ -88,68 +88,79 @@ export class AggregationCursor extends AbstractCursor { /** Add a group stage to the aggregation pipeline */ group($group: Document): this { - this.pipeline.push({ $group }); + assertUninitialized(this); + this[kPipeline].push({ $group }); return this; } /** Add a limit stage to the aggregation pipeline */ limit($limit: number): this { - this.pipeline.push({ $limit }); + assertUninitialized(this); + this[kPipeline].push({ $limit }); return this; } /** Add a match stage to the aggregation pipeline */ match($match: Document): this { - this.pipeline.push({ $match }); + assertUninitialized(this); + this[kPipeline].push({ $match }); return this; } /** Add a out stage to the aggregation pipeline */ out($out: number): this { - this.pipeline.push({ $out }); + assertUninitialized(this); + this[kPipeline].push({ $out }); return this; } /** Add a project stage to the aggregation pipeline */ project($project: Document): this { - this.pipeline.push({ $project }); + assertUninitialized(this); + this[kPipeline].push({ $project }); return this; } /** Add a lookup stage to the aggregation pipeline */ lookup($lookup: Document): this { - this.pipeline.push({ $lookup }); + assertUninitialized(this); + this[kPipeline].push({ $lookup }); return this; } /** Add a redact stage to the aggregation pipeline */ redact($redact: Document): this { - this.pipeline.push({ $redact }); + assertUninitialized(this); + this[kPipeline].push({ $redact }); return this; } /** Add a skip stage to the aggregation pipeline */ skip($skip: number): this { - this.pipeline.push({ $skip }); + assertUninitialized(this); + this[kPipeline].push({ $skip }); return this; } /** Add a sort stage to the aggregation pipeline */ sort($sort: Sort): this { - this.pipeline.push({ $sort }); + assertUninitialized(this); + this[kPipeline].push({ $sort }); return this; } /** Add a unwind stage to the aggregation pipeline */ unwind($unwind: number): this { - this.pipeline.push({ $unwind }); + assertUninitialized(this); + this[kPipeline].push({ $unwind }); return this; } // deprecated methods /** @deprecated Add a geoNear stage to the aggregation pipeline */ geoNear($geoNear: Document): this { - this.pipeline.push({ $geoNear }); + assertUninitialized(this); + this[kPipeline].push({ $geoNear }); return this; } } diff --git a/src/cursor/find_cursor.ts b/src/cursor/find_cursor.ts index 5e64658b51..c08bd1ea56 100644 --- a/src/cursor/find_cursor.ts +++ b/src/cursor/find_cursor.ts @@ -10,7 +10,7 @@ import type { Topology } from '../sdam/topology'; import type { ClientSession } from '../sessions'; import { formatSort, Sort, SortDirection } from '../sort'; import type { Callback, MongoDBNamespace } from '../utils'; -import { AbstractCursor } from './abstract_cursor'; +import { AbstractCursor, assertUninitialized } from './abstract_cursor'; /** @internal */ const kFilter = Symbol('filter'); @@ -51,7 +51,6 @@ export class FindCursor extends AbstractCursor { /** @internal */ _initialize(session: ClientSession | undefined, callback: Callback): void { - this[kBuiltOptions] = Object.freeze(this[kBuiltOptions]); const findOperation = new FindOperation(undefined, this.namespace, this[kFilter], { ...this[kBuiltOptions], // NOTE: order matters here, we may need to refine this ...this.cursorOptions, @@ -151,6 +150,7 @@ export class FindCursor extends AbstractCursor { /** Set the cursor query */ filter(filter: Document): this { + assertUninitialized(this); this[kFilter] = filter; return this; } @@ -161,6 +161,7 @@ export class FindCursor extends AbstractCursor { * @param hint - If specified, then the query system will only consider plans using the hinted index. */ hint(hint: Hint): this { + assertUninitialized(this); this[kBuiltOptions].hint = hint; return this; } @@ -171,6 +172,7 @@ export class FindCursor extends AbstractCursor { * @param min - Specify a $min value to specify the inclusive lower bound for a specific index in order to constrain the results of find(). The $min specifies the lower bound for all keys of a specific index in order. */ min(min: number): this { + assertUninitialized(this); this[kBuiltOptions].min = min; return this; } @@ -181,6 +183,7 @@ export class FindCursor extends AbstractCursor { * @param max - Specify a $max value to specify the exclusive upper bound for a specific index in order to constrain the results of find(). The $max specifies the upper bound for all keys of a specific index in order. */ max(max: number): this { + assertUninitialized(this); this[kBuiltOptions].max = max; return this; } @@ -193,6 +196,7 @@ export class FindCursor extends AbstractCursor { * @param value - the returnKey value. */ returnKey(value: boolean): this { + assertUninitialized(this); this[kBuiltOptions].returnKey = value; return this; } @@ -203,6 +207,7 @@ export class FindCursor extends AbstractCursor { * @param value - The $showDiskLoc option has now been deprecated and replaced with the showRecordId field. $showDiskLoc will still be accepted for OP_QUERY stye find. */ showRecordId(value: boolean): this { + assertUninitialized(this); this[kBuiltOptions].showRecordId = value; return this; } @@ -214,6 +219,7 @@ export class FindCursor extends AbstractCursor { * @param value - The modifier value. */ addQueryModifier(name: string, value: string | boolean | number | Document): this { + assertUninitialized(this); if (name[0] !== '$') { throw new MongoError(`${name} is not a valid query modifier`); } @@ -276,6 +282,7 @@ export class FindCursor extends AbstractCursor { * @param value - The comment attached to this query. */ comment(value: string): this { + assertUninitialized(this); this[kBuiltOptions].comment = value; return this; } @@ -286,6 +293,7 @@ export class FindCursor extends AbstractCursor { * @param value - Number of milliseconds to wait before aborting the tailed query. */ maxAwaitTimeMS(value: number): this { + assertUninitialized(this); if (typeof value !== 'number') { throw new MongoError('maxAwaitTimeMS must be a number'); } @@ -300,6 +308,7 @@ export class FindCursor extends AbstractCursor { * @param value - Number of milliseconds to wait before aborting the query. */ maxTimeMS(value: number): this { + assertUninitialized(this); if (typeof value !== 'number') { throw new MongoError('maxTimeMS must be a number'); } @@ -314,6 +323,7 @@ export class FindCursor extends AbstractCursor { * @param value - The field projection object. */ project(value: Document): this { + assertUninitialized(this); this[kBuiltOptions].projection = value; return this; } @@ -325,6 +335,7 @@ export class FindCursor extends AbstractCursor { * @param direction - The direction of the sorting (1 or -1). */ sort(sort: Sort | string, direction?: SortDirection): this { + assertUninitialized(this); if (this[kBuiltOptions].tailable) { throw new MongoError('Tailable cursor does not support sorting'); } @@ -339,6 +350,7 @@ export class FindCursor extends AbstractCursor { * @param value - The cursor collation options (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields). */ collation(value: CollationOptions): this { + assertUninitialized(this); this[kBuiltOptions].collation = value; return this; } @@ -349,12 +361,13 @@ export class FindCursor extends AbstractCursor { * @param value - The limit for the cursor query. */ limit(value: number): this { + assertUninitialized(this); if (this[kBuiltOptions].tailable) { throw new MongoError('Tailable cursor does not support limit'); } if (typeof value !== 'number') { - throw new MongoError('limit requires an integer'); + throw new TypeError('limit requires an integer'); } this[kBuiltOptions].limit = value; @@ -367,12 +380,13 @@ export class FindCursor extends AbstractCursor { * @param value - The skip for the cursor query. */ skip(value: number): this { + assertUninitialized(this); if (this[kBuiltOptions].tailable) { throw new MongoError('Tailable cursor does not support skip'); } if (typeof value !== 'number') { - throw new MongoError('skip requires an integer'); + throw new TypeError('skip requires an integer'); } this[kBuiltOptions].skip = value; diff --git a/test/functional/cursor.test.js b/test/functional/cursor.test.js index ceb50e1d56..859d24707c 100644 --- a/test/functional/cursor.test.js +++ b/test/functional/cursor.test.js @@ -638,7 +638,7 @@ describe('Cursor', function () { expect(err).to.not.exist; expect(() => { cursor.limit(1); - }).to.throw(/not extensible/); + }).to.throw(/Cursor is already initialized/); done(); }); @@ -868,97 +868,6 @@ describe('Cursor', function () { } }); - it('shouldCorrectlyHandleChangesInBatchSizes', { - // Add a tag that our runner can trigger on - // in this case we are setting that node needs to be higher than 0.10.X to run - metadata: { - requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] } - }, - - test: function (done) { - const configuration = this.configuration; - const client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); - client.connect((err, client) => { - expect(err).to.not.exist; - this.defer(() => client.close()); - - const db = client.db(configuration.db); - db.createCollection('test_not_multiple_batch_size', (err, collection) => { - expect(err).to.not.exist; - - var records = 6; - var batchSize = 2; - var docs = []; - for (var i = 0; i < records; i++) { - docs.push({ a: i }); - } - - collection.insert(docs, configuration.writeConcernMax(), () => { - expect(err).to.not.exist; - - const cursor = collection.find({}, { batchSize: batchSize }); - this.defer(() => cursor.close()); - - //1st - cursor.next((err, items) => { - expect(err).to.not.exist; - //cursor.items should contain 1 since nextObject already popped one - test.equal(1, cursor.bufferedCount()); - test.ok(items != null); - - //2nd - cursor.next((err, items) => { - expect(err).to.not.exist; - test.equal(0, cursor.bufferedCount()); - test.ok(items != null); - - //test batch size modification on the fly - batchSize = 3; - cursor.batchSize(batchSize); - - //3rd - cursor.next((err, items) => { - expect(err).to.not.exist; - test.equal(2, cursor.bufferedCount()); - test.ok(items != null); - - //4th - cursor.next((err, items) => { - expect(err).to.not.exist; - test.equal(1, cursor.bufferedCount()); - test.ok(items != null); - - //5th - cursor.next((err, items) => { - expect(err).to.not.exist; - test.equal(0, cursor.bufferedCount()); - test.ok(items != null); - - //6th - cursor.next((err, items) => { - expect(err).to.not.exist; - test.equal(0, cursor.bufferedCount()); - test.ok(items != null); - - //No more - cursor.next((err, items) => { - expect(err).to.not.exist; - test.ok(items == null); - test.ok(cursor.isClosed()); - done(); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - } - }); - it('shouldCorrectlyHandleBatchSize', { // Add a tag that our runner can trigger on // in this case we are setting that node needs to be higher than 0.10.X to run From f2d1e73ac377d0b55510dce06af957dcc545ae82 Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Fri, 13 Nov 2020 16:03:07 -0500 Subject: [PATCH 06/14] move lodash to dev deps --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 241b015fe5..c73ff6fded 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "bson-ext": "^2.0.0" }, "dependencies": { - "@types/lodash": "^4.14.164", "bl": "^2.2.1", "bson": "^4.0.4", "denque": "^1.4.1", @@ -40,6 +39,7 @@ "@types/bl": "^2.1.0", "@types/bson": "^4.0.2", "@types/kerberos": "^1.1.0", + "@types/lodash": "^4.14.164", "@types/node": "^14.6.4", "@types/saslprep": "^1.0.0", "@typescript-eslint/eslint-plugin": "^3.10.0", From 037e759de583948f20d1f01351ed0af442837de2 Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Fri, 13 Nov 2020 16:03:23 -0500 Subject: [PATCH 07/14] typo `initialized` --- src/cursor/abstract_cursor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 7d4a679127..ae4cf21678 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -17,7 +17,7 @@ const kTopology = Symbol('topology'); const kSession = Symbol('session'); const kOptions = Symbol('options'); const kTransform = Symbol('transform'); -const kInitialized = Symbol('iniitalized'); +const kInitialized = Symbol('initialized'); const kClosed = Symbol('closed'); const kKilled = Symbol('killed'); From c99d32f73fa073fd2187d499fdaa3def368fe8cc Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Fri, 13 Nov 2020 16:03:32 -0500 Subject: [PATCH 08/14] default batchSize to 0 for legacy find path --- src/cmap/wire_protocol/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmap/wire_protocol/query.ts b/src/cmap/wire_protocol/query.ts index 3afebe186b..af84db347a 100644 --- a/src/cmap/wire_protocol/query.ts +++ b/src/cmap/wire_protocol/query.ts @@ -166,7 +166,7 @@ function prepareLegacyFindQuery( options = options || {}; const readPreference = getReadPreference(cmd, options); - const batchSize = cmd.batchSize || options.batchSize; + const batchSize = cmd.batchSize || options.batchSize || 0; const limit = cmd.limit || options.limit; const numberToSkip = cmd.skip || options.skip || 0; From b0d105efe32dfee62c2034f1a297724cdcc40b7b Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Fri, 13 Nov 2020 16:21:34 -0500 Subject: [PATCH 09/14] support custom provided promise --- src/cursor/abstract_cursor.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index ae4cf21678..71b19797cc 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -178,10 +178,7 @@ export abstract class AbstractCursor extends EventEmitter { [Symbol.asyncIterator](): AsyncIterator { return { - next: async () => { - const value = await this.next(); - return { value, done: value === null }; - } + next: () => this.next().then(value => ({ value, done: value === null })) }; } From fe56c1a1255cfc28c0bcfbdf8b3ae1e92d3c8f75 Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Fri, 13 Nov 2020 16:26:10 -0500 Subject: [PATCH 10/14] remove unimplemented test --- test/functional/abstract_cursor.test.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/functional/abstract_cursor.test.js b/test/functional/abstract_cursor.test.js index b813d6acbc..2a33040909 100644 --- a/test/functional/abstract_cursor.test.js +++ b/test/functional/abstract_cursor.test.js @@ -124,14 +124,4 @@ describe('AbstractCursor', function () { }) ); }); - - context('sessions', function () { - it( - 'should end a session after close if the cursor owns the session', - withClientV2(function (client, done) { - // TBD - done(); - }) - ); - }); }); From 5bb33b4ba159bcdae300811d218d37d721d7c714 Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Fri, 13 Nov 2020 16:29:41 -0500 Subject: [PATCH 11/14] make number optional for readBufferedDocuments --- src/cursor/abstract_cursor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 71b19797cc..ecad159948 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -172,8 +172,8 @@ export abstract class AbstractCursor extends EventEmitter { } /** Returns current buffered documents */ - readBufferedDocuments(number: number): Document[] { - return this[kDocuments].splice(0, Math.max(0, Math.min(number, this[kDocuments].length))); + readBufferedDocuments(number?: number): Document[] { + return this[kDocuments].splice(0, number ?? this[kDocuments].length); } [Symbol.asyncIterator](): AsyncIterator { From 207e774c9d5db758902a79685646cce42c9f9d28 Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Wed, 18 Nov 2020 08:55:38 -0500 Subject: [PATCH 12/14] remove `isClosed()` method, depend on `closed` accessor --- package-lock.json | 3 +- src/change_stream.ts | 36 +++++++++++------------ src/cursor/abstract_cursor.ts | 5 ---- src/index.ts | 8 +++++ src/operations/list_collections.ts | 1 + src/sdam/monitor.ts | 1 + test/functional/change_stream.test.js | 12 ++++---- test/functional/cursor.test.js | 20 ++++++------- test/functional/operation_example.test.js | 5 +--- 9 files changed, 46 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5aa2bf381..8f5b9d4665 100644 --- a/package-lock.json +++ b/package-lock.json @@ -537,7 +537,8 @@ "@types/lodash": { "version": "4.14.164", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.164.tgz", - "integrity": "sha512-fXCEmONnrtbYUc5014avwBeMdhHHO8YJCkOBflUL9EoJBSKZ1dei+VO74fA7JkTHZ1GvZack2TyIw5U+1lT8jg==" + "integrity": "sha512-fXCEmONnrtbYUc5014avwBeMdhHHO8YJCkOBflUL9EoJBSKZ1dei+VO74fA7JkTHZ1GvZack2TyIw5U+1lT8jg==", + "dev": true }, "@types/minimist": { "version": "1.2.0", diff --git a/src/change_stream.ts b/src/change_stream.ts index c6b4b41967..62bb3285c4 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -31,6 +31,7 @@ import { executeOperation, ExecutionResult } from './operations/execute_operatio const kResumeQueue = Symbol('resumeQueue'); const kCursorStream = Symbol('cursorStream'); +const kClosed = Symbol('closed'); const CHANGE_STREAM_OPTIONS = ['resumeAfter', 'startAfter', 'startAtOperationTime', 'fullDocument']; const CURSOR_OPTIONS = ['batchSize', 'maxAwaitTimeMS', 'collation', 'readPreference'].concat( @@ -180,10 +181,10 @@ export class ChangeStream extends EventEmitter { namespace: MongoDBNamespace; type: symbol; cursor?: ChangeStreamCursor; - closed: boolean; streamOptions?: CursorStreamOptions; [kResumeQueue]: Denque; [kCursorStream]?: Readable; + [kClosed]: boolean; /** @event */ static readonly CLOSE = 'close' as const; @@ -241,7 +242,7 @@ export class ChangeStream extends EventEmitter { // Create contained Change Stream cursor this.cursor = createChangeStreamCursor(this, options); - this.closed = false; + this[kClosed] = false; // Listen for any `change` listeners being added to ChangeStream this.on('newListener', eventName => { @@ -296,23 +297,20 @@ export class ChangeStream extends EventEmitter { } /** Is the cursor closed */ - isClosed(): boolean { - return this.closed || (this.cursor?.isClosed() ?? false); + get closed(): boolean { + return this[kClosed] || (this.cursor?.closed ?? false); } /** Close the Change Stream */ close(callback?: Callback): Promise | void { - return maybePromise(callback, cb => { - if (this.closed) return cb(); - - // flag the change stream as explicitly closed - this.closed = true; + this[kClosed] = true; - if (!this.cursor) return cb(); + return maybePromise(callback, cb => { + if (!this.cursor) { + return cb(); + } - // Tidy up the existing cursor const cursor = this.cursor; - return cursor.close(err => { endStream(this); this.cursor = undefined; @@ -584,7 +582,7 @@ function processNewChange( change: ChangeStreamDocument, callback?: Callback ) { - if (changeStream.closed) { + if (changeStream[kClosed]) { if (callback) callback(CHANGESTREAM_CLOSED_ERROR); return; } @@ -614,8 +612,8 @@ function processError(changeStream: ChangeStream, error: AnyError, callback?: Ca const cursor = changeStream.cursor; // If the change stream has been closed explicitly, do not process error. - if (changeStream.closed) { - if (callback) callback(new MongoError('ChangeStream is closed')); + if (changeStream[kClosed]) { + if (callback) callback(CHANGESTREAM_CLOSED_ERROR); return; } @@ -674,8 +672,8 @@ function processError(changeStream: ChangeStream, error: AnyError, callback?: Ca * @param changeStream - the parent ChangeStream */ function getCursor(changeStream: ChangeStream, callback: Callback) { - if (changeStream.isClosed()) { - callback(new MongoError('ChangeStream is closed.')); + if (changeStream[kClosed]) { + callback(CHANGESTREAM_CLOSED_ERROR); return; } @@ -698,8 +696,8 @@ function getCursor(changeStream: ChangeStream, callback: Callback changeStream.close()); - assert.equal(changeStream.isClosed(), false); - assert.equal(changeStream.cursor.isClosed(), false); + assert.equal(changeStream.closed, false); + assert.equal(changeStream.cursor.closed, false); changeStream.close(err => { expect(err).to.not.exist; // Check the cursor is closed - assert.equal(changeStream.isClosed(), true); + assert.equal(changeStream.closed, true); assert.ok(!changeStream.cursor); done(); }); @@ -774,7 +774,7 @@ describe('Change Streams', function () { changeStream.hasNext(function (err, hasNext) { expect(err).to.not.exist; assert.equal(hasNext, false); - assert.equal(changeStream.isClosed(), true); + assert.equal(changeStream.closed, true); done(); }); } @@ -841,7 +841,7 @@ describe('Change Streams', function () { .then(() => changeStream.hasNext()) .then(function (hasNext) { assert.equal(hasNext, false); - assert.equal(changeStream.isClosed(), true); + assert.equal(changeStream.closed, true); done(); }); }); @@ -1857,7 +1857,7 @@ describe('Change Streams', function () { afterEach(function () { return Promise.resolve() .then(() => { - if (changeStream && !changeStream.isClosed()) { + if (changeStream && !changeStream.closed) { return changeStream.close(); } }) diff --git a/test/functional/cursor.test.js b/test/functional/cursor.test.js index 859d24707c..8baf03b09e 100644 --- a/test/functional/cursor.test.js +++ b/test/functional/cursor.test.js @@ -927,7 +927,7 @@ describe('Cursor', function () { cursor.next((err, items) => { expect(err).to.not.exist; test.ok(items == null); - test.ok(cursor.isClosed()); + test.ok(cursor.closed); done(); }); }); @@ -993,7 +993,7 @@ describe('Cursor', function () { cursor.next((err, items) => { expect(err).to.not.exist; test.ok(items == null); - test.ok(cursor.isClosed()); + test.ok(cursor.closed); done(); }); }); @@ -1050,7 +1050,7 @@ describe('Cursor', function () { cursor.next((err, items) => { expect(err).to.not.exist; test.ok(items == null); - test.ok(cursor.isClosed()); + test.ok(cursor.closed); done(); }); }); @@ -1208,7 +1208,7 @@ describe('Cursor', function () { const cursor = collection.find(); cursor.close(err => { expect(err).to.not.exist; - test.equal(true, cursor.isClosed()); + test.equal(true, cursor.closed); done(); }); }); @@ -1409,7 +1409,7 @@ describe('Cursor', function () { cursor.close(err => { expect(err).to.not.exist; - test.equal(true, cursor.isClosed()); + test.equal(true, cursor.closed); done(); }); }); @@ -1623,7 +1623,7 @@ describe('Cursor', function () { test.equal(1, closed); test.equal(1, paused); test.equal(1, resumed); - test.strictEqual(cursor.isClosed(), true); + test.strictEqual(cursor.closed, true); done(); } }); @@ -1679,7 +1679,7 @@ describe('Cursor', function () { if (doneCalled === 1) { expect(err).to.not.exist; test.strictEqual(0, i); - test.strictEqual(true, cursor.isClosed()); + test.strictEqual(true, cursor.closed); done(); } }; @@ -1722,7 +1722,7 @@ describe('Cursor', function () { const cursor = collection.find(); const stream = cursor.stream(); - test.strictEqual(false, cursor.isClosed()); + test.strictEqual(false, cursor.closed); stream.on('data', function () { if (++i === 5) { @@ -1740,7 +1740,7 @@ describe('Cursor', function () { test.strictEqual(undefined, err); test.strictEqual(5, i); test.strictEqual(2, finished); - test.strictEqual(true, cursor.isClosed()); + test.strictEqual(true, cursor.closed); done(); } } @@ -1797,7 +1797,7 @@ describe('Cursor', function () { if (finished === 2) { setTimeout(function () { test.equal(5, i); - test.equal(true, cursor.isClosed()); + test.equal(true, cursor.closed); client.close(); configuration.manager.start().then(function () { diff --git a/test/functional/operation_example.test.js b/test/functional/operation_example.test.js index e5b84152a1..cf74113915 100644 --- a/test/functional/operation_example.test.js +++ b/test/functional/operation_example.test.js @@ -5454,9 +5454,6 @@ describe('Operation Examples', function () { /** * A simple example showing the use of the cursor close function. - * - * @example-class Cursor - * @example-method isClosed */ it('shouldStreamDocumentsUsingTheIsCloseFunction', { // Add a tag that our runner can trigger on @@ -5505,7 +5502,7 @@ describe('Operation Examples', function () { // Close the cursor, this is the same as reseting the query cursor.close(function (err) { expect(err).to.not.exist; - test.equal(true, cursor.isClosed()); + test.equal(true, cursor.closed); client.close(done); }); From a68a16471e279dee1993118244101a7d2cca1664 Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Mon, 23 Nov 2020 08:12:55 -0500 Subject: [PATCH 13/14] handle explain for legacy servers --- src/operations/aggregate.ts | 1 - src/operations/find.ts | 12 +++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/operations/aggregate.ts b/src/operations/aggregate.ts index 297a677a86..a8fee700ba 100644 --- a/src/operations/aggregate.ts +++ b/src/operations/aggregate.ts @@ -66,7 +66,6 @@ export class AggregateOperation extends CommandOperation { this.ns.toString(), findCommand, { fullResult: !!this.fullResponse, ...this.options, ...this.bsonOptions }, - callback + (err, result) => { + if (err) return callback(err); + if (this.explain) { + // TODO: NODE-2900 + if (result.documents && result.documents[0]) { + return callback(undefined, result.documents[0]); + } + } + + callback(undefined, result); + } ); } } From bcb37b046fdc048f64c48dea398219af8bfb1c9a Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Mon, 23 Nov 2020 16:04:55 -0500 Subject: [PATCH 14/14] Update test/functional/abstract_cursor.test.js Co-authored-by: Eric Adum --- test/functional/abstract_cursor.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/abstract_cursor.test.js b/test/functional/abstract_cursor.test.js index 2a33040909..52230215b8 100644 --- a/test/functional/abstract_cursor.test.js +++ b/test/functional/abstract_cursor.test.js @@ -50,7 +50,7 @@ describe('AbstractCursor', function () { context('#close', function () { it( - 'should a killCursors command when closed before completely iterated', + 'should send a killCursors command when closed before completely iterated', withClientV2(function (client, done) { const commands = []; client.on('commandStarted', filterForCommands(['killCursors'], commands));