Skip to content

Commit 1b881b0

Browse files
nbbeekenljhaywar
authored andcommitted
fix(NODE-3343): allow overriding result document after projection applied (#2856)
1 parent c7e86b1 commit 1b881b0

File tree

4 files changed

+64
-13
lines changed

4 files changed

+64
-13
lines changed

src/cursor/abstract_cursor.ts

+1
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ export abstract class AbstractCursor<
483483
* a new instance of a cursor. This means when calling map, you should always assign the result to a new
484484
* variable. Take note of the following example:
485485
*
486+
* @example
486487
* ```typescript
487488
* const cursor: FindCursor<Document> = coll.find();
488489
* const mappedCursor: FindCursor<number> = cursor.map(doc => Object.keys(doc).length);

src/cursor/aggregation_cursor.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { ClientSession } from '../sessions';
1010
import type { OperationParent } from '../operations/command';
1111
import type { AbstractCursorOptions } from './abstract_cursor';
1212
import type { ExplainVerbosityLike } from '../explain';
13+
import type { Projection } from '../mongo_types';
1314

1415
/** @public */
1516
export interface AggregationCursorOptions extends AbstractCursorOptions, AggregateOptions {}
@@ -134,8 +135,25 @@ export class AggregationCursor<TSchema = Document> extends AbstractCursor<TSchem
134135
return this;
135136
}
136137

137-
/** Add a project stage to the aggregation pipeline */
138-
project<T = TSchema>($project: Document): AggregationCursor<T>;
138+
/**
139+
* Add a project stage to the aggregation pipeline
140+
*
141+
* @remarks
142+
* In order to strictly type this function you must provide an interface
143+
* that represents the effect of your projection on the result documents.
144+
*
145+
* **NOTE:** adding a projection changes the return type of the iteration of this cursor,
146+
* it **does not** return a new instance of a cursor. This means when calling project,
147+
* you should always assign the result to a new variable. Take note of the following example:
148+
*
149+
* @example
150+
* ```typescript
151+
* const cursor: AggregationCursor<{ a: number; b: string }> = coll.aggregate([]);
152+
* const projectCursor = cursor.project<{ a: number }>({ a: true });
153+
* const aPropOnlyArray: {a: number}[] = await projectCursor.toArray();
154+
* ```
155+
*/
156+
project<T = TSchema>($project: Projection<T>): AggregationCursor<T>;
139157
project($project: Document): this {
140158
assertUninitialized(this);
141159
this[kPipeline].push({ $project });

src/cursor/find_cursor.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { ClientSession } from '../sessions';
1212
import { formatSort, Sort, SortDirection } from '../sort';
1313
import type { Callback, MongoDBNamespace } from '../utils';
1414
import { AbstractCursor, assertUninitialized } from './abstract_cursor';
15-
import type { Projection, ProjectionOperators, SchemaMember } from '../mongo_types';
15+
import type { Projection } from '../mongo_types';
1616

1717
/** @internal */
1818
const kFilter = Symbol('filter');
@@ -338,12 +338,24 @@ export class FindCursor<TSchema = Document> extends AbstractCursor<TSchema> {
338338
}
339339

340340
/**
341-
* Sets a field projection for the query.
341+
* Add a project stage to the aggregation pipeline
342342
*
343-
* @param value - The field projection object.
343+
* @remarks
344+
* In order to strictly type this function you must provide an interface
345+
* that represents the effect of your projection on the result documents.
346+
*
347+
* **NOTE:** adding a projection changes the return type of the iteration of this cursor,
348+
* it **does not** return a new instance of a cursor. This means when calling project,
349+
* you should always assign the result to a new variable. Take note of the following example:
350+
*
351+
* @example
352+
* ```typescript
353+
* const cursor: FindCursor<{ a: number; b: string }> = coll.find();
354+
* const projectCursor = cursor.project<{ a: number }>({ a: true });
355+
* const aPropOnlyArray: {a: number}[] = await projectCursor.toArray();
356+
* ```
344357
*/
345-
// TODO(NODE-3343): add parameterized cursor return type
346-
project<T = TSchema>(value: SchemaMember<T, ProjectionOperators | number | boolean | any>): this;
358+
project<T = TSchema>(value: Projection<T>): FindCursor<T>;
347359
project(value: Projection<TSchema>): this {
348360
assertUninitialized(this);
349361
this[kBuiltOptions].projection = value;

test/types/community/cursor.test-d.ts

+26-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Readable } from 'stream';
2-
import { expectType } from 'tsd';
2+
import { expectNotType, expectType } from 'tsd';
33
import { FindCursor, MongoClient } from '../../../src/index';
44

55
// TODO(NODE-3346): Improve these tests to use expect assertions more
@@ -40,6 +40,7 @@ collection.find().sort({});
4040
interface TypedDoc {
4141
name: string;
4242
age: number;
43+
listOfNumbers: number[];
4344
tag: {
4445
name: string;
4546
};
@@ -65,12 +66,31 @@ typedCollection
6566
.map(x => x.name2 && x.age2);
6667
typedCollection.find({ name: '123' }, { projection: { age: 1 } }).map(x => x.tag);
6768

68-
typedCollection.find().project({ name: 1 });
69-
typedCollection.find().project({ notExistingField: 1 });
70-
typedCollection.find().project({ max: { $max: [] } });
69+
// A known key with a constant projection
70+
expectType<{ name: string }[]>(await typedCollection.find().project({ name: 1 }).toArray());
71+
expectNotType<{ age: number }[]>(await typedCollection.find().project({ name: 1 }).toArray());
7172

72-
// $ExpectType Cursor<{ name: string; }>
73-
typedCollection.find().project<{ name: string }>({ name: 1 });
73+
// An unknown key
74+
expectType<{ notExistingField: unknown }[]>(
75+
await typedCollection.find().project({ notExistingField: 1 }).toArray()
76+
);
77+
expectNotType<TypedDoc[]>(await typedCollection.find().project({ notExistingField: 1 }).toArray());
78+
79+
// Projection operator
80+
expectType<{ listOfNumbers: number[] }[]>(
81+
await typedCollection
82+
.find()
83+
.project({ listOfNumbers: { $slice: [0, 4] } })
84+
.toArray()
85+
);
86+
87+
// Using the override parameter works
88+
expectType<{ name: string }[]>(
89+
await typedCollection
90+
.find()
91+
.project<{ name: string }>({ name: 1 })
92+
.toArray()
93+
);
7494

7595
void async function () {
7696
for await (const item of cursor) {

0 commit comments

Comments
 (0)