Skip to content

fix(NODE-3343): allow overriding result document after projection applied #2856

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cursor/abstract_cursor.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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