@@ -4,6 +4,7 @@ import { promisify } from 'util';
4
4
import { BSONSerializeOptions , Document , Long , pluckBSONSerializeOptions } from '../bson' ;
5
5
import {
6
6
AnyError ,
7
+ MongoAPIError ,
7
8
MongoCursorExhaustedError ,
8
9
MongoCursorInUseError ,
9
10
MongoInvalidArgumentError ,
@@ -305,7 +306,18 @@ export abstract class AbstractCursor<
305
306
while ( true ) {
306
307
const document = await this . next ( ) ;
307
308
308
- if ( document == null ) {
309
+ // Intentional strict null check, because users can map cursors to falsey values.
310
+ // We allow mapping to all values except for null.
311
+ // eslint-disable-next-line no-restricted-syntax
312
+ if ( document === null ) {
313
+ if ( ! this . closed ) {
314
+ const message =
315
+ 'Cursor returned a `null` document, but the cursor is not exhausted. Mapping documents to `null` is not supported in the cursor transform.' ;
316
+
317
+ await cleanupCursorAsync ( this , { needsToEmitClosed : true } ) . catch ( ( ) => null ) ;
318
+
319
+ throw new MongoAPIError ( message ) ;
320
+ }
309
321
break ;
310
322
}
311
323
@@ -504,6 +516,29 @@ export abstract class AbstractCursor<
504
516
* this function's transform.
505
517
*
506
518
* @remarks
519
+ *
520
+ * **Note** Cursors use `null` internally to indicate that there are no more documents in the cursor. Providing a mapping
521
+ * function that maps values to `null` will result in the cursor closing itself before it has finished iterating
522
+ * all documents. This will **not** result in a memory leak, just surprising behavior. For example:
523
+ *
524
+ * ```typescript
525
+ * const cursor = collection.find({});
526
+ * cursor.map(() => null);
527
+ *
528
+ * const documents = await cursor.toArray();
529
+ * // documents is always [], regardless of how many documents are in the collection.
530
+ * ```
531
+ *
532
+ * Other falsey values are allowed:
533
+ *
534
+ * ```typescript
535
+ * const cursor = collection.find({});
536
+ * cursor.map(() => '');
537
+ *
538
+ * const documents = await cursor.toArray();
539
+ * // documents is now an array of empty strings
540
+ * ```
541
+ *
507
542
* **Note for Typescript Users:** adding a transform changes the return type of the iteration of this cursor,
508
543
* it **does not** return a new instance of a cursor. This means when calling map,
509
544
* you should always assign the result to a new variable in order to get a correctly typed cursor variable.
@@ -657,7 +692,7 @@ export abstract class AbstractCursor<
657
692
* a significant refactor.
658
693
*/
659
694
[ kInit ] ( callback : Callback < TSchema | null > ) : void {
660
- this . _initialize ( this [ kSession ] , ( err , state ) => {
695
+ this . _initialize ( this [ kSession ] , ( error , state ) => {
661
696
if ( state ) {
662
697
const response = state . response ;
663
698
this [ kServer ] = state . server ;
@@ -689,8 +724,12 @@ export abstract class AbstractCursor<
689
724
// the cursor is now initialized, even if an error occurred or it is dead
690
725
this [ kInitialized ] = true ;
691
726
692
- if ( err || cursorIsDead ( this ) ) {
693
- return cleanupCursor ( this , { error : err } , ( ) => callback ( err , nextDocument ( this ) ) ) ;
727
+ if ( error ) {
728
+ return cleanupCursor ( this , { error } , ( ) => callback ( error , undefined ) ) ;
729
+ }
730
+
731
+ if ( cursorIsDead ( this ) ) {
732
+ return cleanupCursor ( this , undefined , ( ) => callback ( ) ) ;
694
733
}
695
734
696
735
callback ( ) ;
@@ -743,11 +782,8 @@ export function next<T>(
743
782
744
783
if ( cursorId == null ) {
745
784
// All cursors must operate within a session, one must be made implicitly if not explicitly provided
746
- cursor [ kInit ] ( ( err , value ) => {
785
+ cursor [ kInit ] ( err => {
747
786
if ( err ) return callback ( err ) ;
748
- if ( value ) {
749
- return callback ( undefined , value ) ;
750
- }
751
787
return next ( cursor , blocking , callback ) ;
752
788
} ) ;
753
789
0 commit comments