Skip to content

Commit

Permalink
fix(NODE-3343): allow overriding result document after projection app…
Browse files Browse the repository at this point in the history
…lied (#2856)
  • Loading branch information
nbbeeken committed Jun 25, 2021
1 parent dfb91b8 commit 988f9c8
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 13 deletions.
1 change: 1 addition & 0 deletions src/cursor/abstract_cursor.ts
Expand Up @@ -483,6 +483,7 @@ export abstract class AbstractCursor<
* a new instance of a cursor. This means when calling map, you should always assign the result to a new
* variable. Take note of the following example:
*
* @example
* ```typescript
* const cursor: FindCursor<Document> = coll.find();
* const mappedCursor: FindCursor<number> = cursor.map(doc => Object.keys(doc).length);
Expand Down
22 changes: 20 additions & 2 deletions src/cursor/aggregation_cursor.ts
Expand Up @@ -10,6 +10,7 @@ import type { ClientSession } from '../sessions';
import type { OperationParent } from '../operations/command';
import type { AbstractCursorOptions } from './abstract_cursor';
import type { ExplainVerbosityLike } from '../explain';
import type { Projection } from '../mongo_types';

/** @public */
export interface AggregationCursorOptions extends AbstractCursorOptions, AggregateOptions {}
Expand Down Expand Up @@ -134,8 +135,25 @@ export class AggregationCursor<TSchema = Document> extends AbstractCursor<TSchem
return this;
}

/** Add a project stage to the aggregation pipeline */
project<T = TSchema>($project: Document): AggregationCursor<T>;
/**
* Add a project stage to the aggregation pipeline
*
* @remarks
* In order to strictly type this function you must provide an interface
* that represents the effect of your projection on the result documents.
*
* **NOTE:** adding a projection changes the return type of the iteration of this cursor,
* it **does not** return a new instance of a cursor. This means when calling project,
* you should always assign the result to a new variable. Take note of the following example:
*
* @example
* ```typescript
* const cursor: AggregationCursor<{ a: number; b: string }> = coll.aggregate([]);
* const projectCursor = cursor.project<{ a: number }>({ a: true });
* const aPropOnlyArray: {a: number}[] = await projectCursor.toArray();
* ```
*/
project<T = TSchema>($project: Projection<T>): AggregationCursor<T>;
project($project: Document): this {
assertUninitialized(this);
this[kPipeline].push({ $project });
Expand Down
22 changes: 17 additions & 5 deletions src/cursor/find_cursor.ts
Expand Up @@ -12,7 +12,7 @@ import type { ClientSession } from '../sessions';
import { formatSort, Sort, SortDirection } from '../sort';
import type { Callback, MongoDBNamespace } from '../utils';
import { AbstractCursor, assertUninitialized } from './abstract_cursor';
import type { Projection, ProjectionOperators, SchemaMember } from '../mongo_types';
import type { Projection } from '../mongo_types';

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

/**
* Sets a field projection for the query.
* Add a project stage to the aggregation pipeline
*
* @param value - The field projection object.
* @remarks
* In order to strictly type this function you must provide an interface
* that represents the effect of your projection on the result documents.
*
* **NOTE:** adding a projection changes the return type of the iteration of this cursor,
* it **does not** return a new instance of a cursor. This means when calling project,
* you should always assign the result to a new variable. Take note of the following example:
*
* @example
* ```typescript
* const cursor: FindCursor<{ a: number; b: string }> = coll.find();
* const projectCursor = cursor.project<{ a: number }>({ a: true });
* const aPropOnlyArray: {a: number}[] = await projectCursor.toArray();
* ```
*/
// TODO(NODE-3343): add parameterized cursor return type
project<T = TSchema>(value: SchemaMember<T, ProjectionOperators | number | boolean | any>): this;
project<T = TSchema>(value: Projection<T>): FindCursor<T>;
project(value: Projection<TSchema>): this {
assertUninitialized(this);
this[kBuiltOptions].projection = value;
Expand Down
32 changes: 26 additions & 6 deletions test/types/community/cursor.test-d.ts
@@ -1,5 +1,5 @@
import type { Readable } from 'stream';
import { expectType } from 'tsd';
import { expectNotType, expectType } from 'tsd';
import { FindCursor, MongoClient } from '../../../src/index';

// TODO(NODE-3346): Improve these tests to use expect assertions more
Expand Down Expand Up @@ -40,6 +40,7 @@ collection.find().sort({});
interface TypedDoc {
name: string;
age: number;
listOfNumbers: number[];
tag: {
name: string;
};
Expand All @@ -65,12 +66,31 @@ typedCollection
.map(x => x.name2 && x.age2);
typedCollection.find({ name: '123' }, { projection: { age: 1 } }).map(x => x.tag);

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

// $ExpectType Cursor<{ name: string; }>
typedCollection.find().project<{ name: string }>({ name: 1 });
// An unknown key
expectType<{ notExistingField: unknown }[]>(
await typedCollection.find().project({ notExistingField: 1 }).toArray()
);
expectNotType<TypedDoc[]>(await typedCollection.find().project({ notExistingField: 1 }).toArray());

// Projection operator
expectType<{ listOfNumbers: number[] }[]>(
await typedCollection
.find()
.project({ listOfNumbers: { $slice: [0, 4] } })
.toArray()
);

// Using the override parameter works
expectType<{ name: string }[]>(
await typedCollection
.find()
.project<{ name: string }>({ name: 1 })
.toArray()
);

void async function () {
for await (const item of cursor) {
Expand Down

0 comments on commit 988f9c8

Please sign in to comment.