From e539c43ddbeca0452b0706c58918a9c37448f1d4 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 22 Jun 2021 17:25:05 -0400 Subject: [PATCH 1/4] fix(NODE-3343): allow overriding result document after projection applied --- src/cursor/abstract_cursor.ts | 1 + src/cursor/aggregation_cursor.ts | 22 ++++++++++++++++++++-- src/cursor/find_cursor.ts | 20 ++++++++++++++++---- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 3c8cfd00506..e2addc9738a 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 fa3e86df0dd..12f50c31551 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 9add956c551..cc17a543555 100644 --- a/src/cursor/find_cursor.ts +++ b/src/cursor/find_cursor.ts @@ -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; From 917c0f9bc7b9ad53ee141d4c2c26e4525fca9ff7 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 24 Jun 2021 09:01:23 -0400 Subject: [PATCH 2/4] fix: lint --- src/cursor/find_cursor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cursor/find_cursor.ts b/src/cursor/find_cursor.ts index cc17a543555..672ad38b1f6 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'); From c2956562afdb7f77bf31c6618da398a6f562a22a Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 24 Jun 2021 14:35:28 -0400 Subject: [PATCH 3/4] test: cursor projection types --- test/types/community/cursor.test-d.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/types/community/cursor.test-d.ts b/test/types/community/cursor.test-d.ts index 286328af7dc..93e6d0ad447 100644 --- a/test/types/community/cursor.test-d.ts +++ b/test/types/community/cursor.test-d.ts @@ -65,12 +65,12 @@ 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: [] } }); - -// $ExpectType Cursor<{ name: string; }> -typedCollection.find().project<{ name: string }>({ name: 1 }); +expectType>(typedCollection.find().project({ name: 1 })); +expectType>(typedCollection.find().project({ notExistingField: 1 })); +expectType>(typedCollection.find().project({ max: { $max: [] } })); +expectType>( + typedCollection.find().project<{ name: string }>({ name: 1 }) +); void async function () { for await (const item of cursor) { From 0ab577aef1ccba82559bdef6d28d1da866378fa7 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 24 Jun 2021 17:02:05 -0400 Subject: [PATCH 4/4] fix: type errors and added some not expected cases --- test/types/community/cursor.test-d.ts | 32 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/test/types/community/cursor.test-d.ts b/test/types/community/cursor.test-d.ts index 93e6d0ad447..ba1b13d8f8e 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,11 +66,30 @@ typedCollection .map(x => x.name2 && x.age2); typedCollection.find({ name: '123' }, { projection: { age: 1 } }).map(x => x.tag); -expectType>(typedCollection.find().project({ name: 1 })); -expectType>(typedCollection.find().project({ notExistingField: 1 })); -expectType>(typedCollection.find().project({ max: { $max: [] } })); -expectType>( - typedCollection.find().project<{ name: string }>({ name: 1 }) +// 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()); + +// 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 () {