diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 3c8cfd0050..e2addc9738 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -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 = coll.find(); * const mappedCursor: FindCursor = cursor.map(doc => Object.keys(doc).length); diff --git a/src/cursor/aggregation_cursor.ts b/src/cursor/aggregation_cursor.ts index fa3e86df0d..12f50c3155 100644 --- a/src/cursor/aggregation_cursor.ts +++ b/src/cursor/aggregation_cursor.ts @@ -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 {} @@ -134,8 +135,25 @@ export class AggregationCursor extends AbstractCursor($project: Document): AggregationCursor; + /** + * 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($project: Projection): AggregationCursor; project($project: Document): this { assertUninitialized(this); this[kPipeline].push({ $project }); diff --git a/src/cursor/find_cursor.ts b/src/cursor/find_cursor.ts index 9add956c55..672ad38b1f 100644 --- a/src/cursor/find_cursor.ts +++ b/src/cursor/find_cursor.ts @@ -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'); @@ -338,12 +338,24 @@ export class FindCursor extends AbstractCursor { } /** - * 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(value: SchemaMember): this; + project(value: Projection): FindCursor; project(value: Projection): this { assertUninitialized(this); this[kBuiltOptions].projection = value; diff --git a/test/types/community/cursor.test-d.ts b/test/types/community/cursor.test-d.ts index 286328af7d..ba1b13d8f8 100644 --- a/test/types/community/cursor.test-d.ts +++ b/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 @@ -40,6 +40,7 @@ collection.find().sort({}); interface TypedDoc { name: string; age: number; + listOfNumbers: number[]; tag: { name: string; }; @@ -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(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) {