From b57859c3a4c091b0afebb0d03f8218c872e165ca Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 30 Jul 2021 13:03:49 -0400 Subject: [PATCH 1/9] fix(NODE-3454): projection types are too narrow --- src/collection.ts | 16 +++---- src/cursor/aggregation_cursor.ts | 10 ++++- src/cursor/find_cursor.ts | 12 +++-- src/index.ts | 2 - src/mongo_types.ts | 15 ------- src/operations/find.ts | 5 +-- .../community/collection/findX.test-d.ts | 8 ++-- test/types/community/cursor.test-d.ts | 45 +++++++++++++++++-- 8 files changed, 73 insertions(+), 40 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index da41895076..58f74dde20 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -676,10 +676,10 @@ export class Collection { findOne(callback: Callback): void; findOne(filter: Filter): Promise; findOne(filter: Filter, callback: Callback): void; - findOne(filter: Filter, options: FindOptions): Promise; + findOne(filter: Filter, options: FindOptions): Promise; findOne( filter: Filter, - options: FindOptions, + options: FindOptions, callback: Callback ): void; @@ -687,16 +687,16 @@ export class Collection { findOne(): Promise; findOne(callback: Callback): void; findOne(filter: Filter): Promise; - findOne(filter: Filter, options?: FindOptions): Promise; + findOne(filter: Filter, options?: FindOptions): Promise; findOne( filter: Filter, - options?: FindOptions, + options?: FindOptions, callback?: Callback ): void; findOne( filter?: Filter | Callback, - options?: FindOptions | Callback, + options?: FindOptions | Callback, callback?: Callback ): Promise | void { if (callback != null && typeof callback !== 'function') { @@ -728,9 +728,9 @@ export class Collection { * @param filter - The filter predicate. If unspecified, then all documents in the collection will match the predicate */ find(): FindCursor; - find(filter: Filter, options?: FindOptions): FindCursor; - find(filter: Filter, options?: FindOptions): FindCursor; - find(filter?: Filter, options?: FindOptions): FindCursor { + find(filter: Filter, options?: FindOptions): FindCursor; + find(filter: Filter, options?: FindOptions): FindCursor; + find(filter?: Filter, options?: FindOptions): FindCursor { if (arguments.length > 2) { throw new MongoInvalidArgumentError( 'Method "collection.find()" accepts at most two arguments' diff --git a/src/cursor/aggregation_cursor.ts b/src/cursor/aggregation_cursor.ts index d01428dd79..8d6bfab39e 100644 --- a/src/cursor/aggregation_cursor.ts +++ b/src/cursor/aggregation_cursor.ts @@ -9,7 +9,6 @@ import type { Callback, MongoDBNamespace } from '../utils'; import type { ClientSession } from '../sessions'; import type { AbstractCursorOptions } from './abstract_cursor'; import type { ExplainVerbosityLike } from '../explain'; -import type { Projection } from '../mongo_types'; /** @public */ export interface AggregationCursorOptions extends AbstractCursorOptions, AggregateOptions {} @@ -144,9 +143,16 @@ export class AggregationCursor extends AbstractCursor = coll.aggregate([]); * const projectCursor = cursor.project<{ a: number }>({ a: true }); * const aPropOnlyArray: {a: number}[] = await projectCursor.toArray(); + * + * // or always use chaining and save the final cursor + * + * const cursor = coll.aggregate<{ a: number }>() + * .project<{ a: string }>( + * { a: { $convert: { input: '$a' to: 'string' } }} + * ); * ``` */ - project($project: Projection): AggregationCursor; + project($project: Document): 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 65821f8f5f..d7e88393bb 100644 --- a/src/cursor/find_cursor.ts +++ b/src/cursor/find_cursor.ts @@ -12,7 +12,6 @@ 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 } from '../mongo_types'; /** @internal */ const kFilter = Symbol('filter'); @@ -353,10 +352,17 @@ export class FindCursor extends AbstractCursor { * const cursor: FindCursor<{ a: number; b: string }> = coll.find(); * const projectCursor = cursor.project<{ a: number }>({ a: true }); * const aPropOnlyArray: {a: number}[] = await projectCursor.toArray(); + * + * // or always use chaining and save the final cursor + * + * const cursor = coll.find() + * .project<{ a: string }>( + * { a: { $convert: { input: '$a' to: 'string' } }} + * ); * ``` */ - project(value: Projection): FindCursor; - project(value: Projection): this { + project(value: Document): FindCursor; + project(value: Document): this { assertUninitialized(this); this[kBuiltOptions].projection = value; return this; diff --git a/src/index.ts b/src/index.ts index 870bb63a11..c568e31e59 100644 --- a/src/index.ts +++ b/src/index.ts @@ -371,9 +371,7 @@ export type { WithoutId, UpdateFilter, Filter, - Projection, InferIdType, - ProjectionOperators, Flatten, SchemaMember, Condition, diff --git a/src/mongo_types.ts b/src/mongo_types.ts index cf25f75d66..5299e60733 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -169,21 +169,6 @@ export type BSONType = typeof BSONType[keyof typeof BSONType]; /** @public */ export type BSONTypeAlias = keyof typeof BSONType; -/** @public */ -export interface ProjectionOperators extends Document { - $elemMatch?: Document; - $slice?: number | [number, number]; - $meta?: string; - /** @deprecated Since MongoDB 3.2, Use FindCursor#max */ - $max?: any; -} - -/** @public */ -export type Projection = { - [Key in keyof TSchema]?: ProjectionOperators | 0 | 1 | boolean; -} & - Partial>; - /** @public */ export type IsAny = true extends false & Type ? ResultIfAny diff --git a/src/operations/find.ts b/src/operations/find.ts index d3eed86a36..af09ada385 100644 --- a/src/operations/find.ts +++ b/src/operations/find.ts @@ -15,16 +15,15 @@ import { Sort, formatSort } from '../sort'; import { isSharded } from '../cmap/wire_protocol/shared'; import { ReadConcern } from '../read_concern'; import type { ClientSession } from '../sessions'; -import type { Projection } from '../mongo_types'; /** @public */ -export interface FindOptions extends CommandOperationOptions { +export interface FindOptions extends CommandOperationOptions { /** Sets the limit of documents returned in the query. */ limit?: number; /** Set to sort the documents coming back from the query. Array of indexes, `[['a', 1]]` etc. */ sort?: Sort; /** The fields to return in the query. Object of fields to either include or exclude (one of, not both), `{'a':1, 'b': 1}` **or** `{'a': 0, 'b': 0}` */ - projection?: Projection; + projection?: Document; /** Set to skip N documents ahead in your query (useful for pagination). */ skip?: number; /** Tell the query to use specific indexes in the query. Object of indexes to use, `{'_id':1}` */ diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index bc17e3179e..5fd57acc52 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -35,7 +35,7 @@ await collectionT.findOne( } ); -const optionsWithComplexProjection: FindOptions = { +const optionsWithComplexProjection: FindOptions = { projection: { stringField: { $meta: 'textScore' }, fruitTags: { $min: 'fruitTags' }, @@ -120,14 +120,14 @@ function printCar(car: Car | undefined) { console.log(car ? `A car of ${car.make} make` : 'No car'); } -const options: FindOptions = {}; -const optionsWithProjection: FindOptions = { +const options: FindOptions = {}; +const optionsWithProjection: FindOptions = { projection: { make: 1 } }; -expectNotType>({ +expectNotType({ projection: { make: 'invalid' } diff --git a/test/types/community/cursor.test-d.ts b/test/types/community/cursor.test-d.ts index 2ea825b388..c63b2e8e84 100644 --- a/test/types/community/cursor.test-d.ts +++ b/test/types/community/cursor.test-d.ts @@ -1,6 +1,6 @@ import type { Readable } from 'stream'; import { expectNotType, expectType } from 'tsd'; -import { FindCursor, MongoClient } from '../../../src/index'; +import { FindCursor, MongoClient, Db } from '../../../src/index'; // TODO(NODE-3346): Improve these tests to use expect assertions more @@ -74,13 +74,13 @@ expectNotType<{ age: number }[]>(await typedCollection.find().project({ name: 1 expectType<{ notExistingField: unknown }[]>( await typedCollection.find().project({ notExistingField: 1 }).toArray() ); -expectNotType(await typedCollection.find().project({ notExistingField: 1 }).toArray()); +expectType(await typedCollection.find().project({ notExistingField: 1 }).toArray()); // Projection operator expectType<{ listOfNumbers: number[] }[]>( await typedCollection .find() - .project({ listOfNumbers: { $slice: [0, 4] } }) + .project<{ listOfNumbers: number[] }>({ listOfNumbers: { $slice: [0, 4] } }) .toArray() ); @@ -98,3 +98,42 @@ void async function () { expectType(item.foo); } }; + +interface InternalMeme { + _id: string; + owner: string; + receiver: string; + createdAt: Date; + expiredAt: Date; + description: string; + likes: string; + private: string; + replyTo: string; + imageUrl: string; +} + +interface PublicMeme { + myId: string; + owner: string; + likes: number; + someRandomProp: boolean; // Projection makes no enforcement on anything + // the convenience parameter project() allows you to define a return type, + // otherwise projections returns a generic Document +} + +const publicMemeProjection = { + myId: { $toString: '$_id' }, + owner: { $toString: '$owner' }, + receiver: { $toString: '$receiver' }, + likes: '$totalLikes' // <== cause of TS2345 error +}; + +expectType( + await new Db(new MongoClient(''), '') + .collection('memes') + .find({ _id: { $in: [] } }) + .sort({ _id: -1 }) + .limit(3) + .project(publicMemeProjection) // <== location of TS2345 error + .toArray() +); From 6810be30281a9fb1512f638f9876c6319a18bdf4 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 30 Jul 2021 15:03:17 -0400 Subject: [PATCH 2/9] revert breakages --- src/index.ts | 2 ++ src/mongo_types.ts | 5 +++++ src/operations/find.ts | 5 +++-- test/types/community/collection/findX.test-d.ts | 9 ++++++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index c568e31e59..870bb63a11 100644 --- a/src/index.ts +++ b/src/index.ts @@ -371,7 +371,9 @@ export type { WithoutId, UpdateFilter, Filter, + Projection, InferIdType, + ProjectionOperators, Flatten, SchemaMember, Condition, diff --git a/src/mongo_types.ts b/src/mongo_types.ts index 5299e60733..2f02ab944a 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -169,6 +169,11 @@ export type BSONType = typeof BSONType[keyof typeof BSONType]; /** @public */ export type BSONTypeAlias = keyof typeof BSONType; +/** @public Projection is flexible to permit the wide array of aggregation operators */ +export type Projection = Document; +/** @public */ +export type ProjectionOperators = Document; + /** @public */ export type IsAny = true extends false & Type ? ResultIfAny diff --git a/src/operations/find.ts b/src/operations/find.ts index af09ada385..b29bdb4de1 100644 --- a/src/operations/find.ts +++ b/src/operations/find.ts @@ -15,15 +15,16 @@ import { Sort, formatSort } from '../sort'; import { isSharded } from '../cmap/wire_protocol/shared'; import { ReadConcern } from '../read_concern'; import type { ClientSession } from '../sessions'; +import type { Projection } from '../mongo_types'; /** @public */ -export interface FindOptions extends CommandOperationOptions { +export interface FindOptions extends CommandOperationOptions { /** Sets the limit of documents returned in the query. */ limit?: number; /** Set to sort the documents coming back from the query. Array of indexes, `[['a', 1]]` etc. */ sort?: Sort; /** The fields to return in the query. Object of fields to either include or exclude (one of, not both), `{'a':1, 'b': 1}` **or** `{'a': 0, 'b': 0}` */ - projection?: Document; + projection?: Projection; /** Set to skip N documents ahead in your query (useful for pagination). */ skip?: number; /** Tell the query to use specific indexes in the query. Object of indexes to use, `{'_id':1}` */ diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 5fd57acc52..2730be12ed 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -1,5 +1,6 @@ -import { expectNotType, expectType } from 'tsd'; +import { expectAssignable, expectNotType, expectType } from 'tsd'; import { FindCursor, FindOptions, MongoClient, Document } from '../../../../src'; +import { Projection, ProjectionOperators } from '../../../../src'; import type { PropExists } from '../../utility_types'; // collection.findX tests @@ -190,3 +191,9 @@ expectType>(colorCollection.find({ color: // When you use the override, $in doesn't permit readonly colorCollection.find<{ color: string }>({ color: { $in: colorsFreeze } }); colorCollection.find<{ color: string }>({ color: { $in: ['regularArray'] } }); +// This is a regression test that we don't remove the unused generic in FindOptions +const findOptions: FindOptions<{ a: number }> = {}; +expectType(findOptions); +// This is just to check that we still export these type symbols +expectAssignable({}); +expectAssignable({}); From fa9c234f741b61c8752fca6c6b12c82332af3dec Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 30 Jul 2021 16:20:10 -0400 Subject: [PATCH 3/9] fix: type import --- test/types/community/collection/findX.test-d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 2730be12ed..06ee9b95d0 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -1,6 +1,6 @@ import { expectAssignable, expectNotType, expectType } from 'tsd'; import { FindCursor, FindOptions, MongoClient, Document } from '../../../../src'; -import { Projection, ProjectionOperators } from '../../../../src'; +import type { Projection, ProjectionOperators } from '../../../../src'; import type { PropExists } from '../../utility_types'; // collection.findX tests From 8bf04f450514bc3fae9e6c9b79648422169709eb Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 3 Aug 2021 13:03:20 -0400 Subject: [PATCH 4/9] fix: lint and tests --- src/mongo_types.ts | 1 + test/types/community/cursor.test-d.ts | 29 +++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/mongo_types.ts b/src/mongo_types.ts index 2f02ab944a..4abff4e53e 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -170,6 +170,7 @@ export type BSONType = typeof BSONType[keyof typeof BSONType]; export type BSONTypeAlias = keyof typeof BSONType; /** @public Projection is flexible to permit the wide array of aggregation operators */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars export type Projection = Document; /** @public */ export type ProjectionOperators = Document; diff --git a/test/types/community/cursor.test-d.ts b/test/types/community/cursor.test-d.ts index c63b2e8e84..aa5d6d1363 100644 --- a/test/types/community/cursor.test-d.ts +++ b/test/types/community/cursor.test-d.ts @@ -1,6 +1,6 @@ import type { Readable } from 'stream'; import { expectNotType, expectType } from 'tsd'; -import { FindCursor, MongoClient, Db } from '../../../src/index'; +import { FindCursor, MongoClient, Db, Document } from '../../../src/index'; // TODO(NODE-3346): Improve these tests to use expect assertions more @@ -118,7 +118,7 @@ interface PublicMeme { likes: number; someRandomProp: boolean; // Projection makes no enforcement on anything // the convenience parameter project() allows you to define a return type, - // otherwise projections returns a generic Document + // otherwise projections returns your schema } const publicMemeProjection = { @@ -127,13 +127,34 @@ const publicMemeProjection = { receiver: { $toString: '$receiver' }, likes: '$totalLikes' // <== cause of TS2345 error }; +const memeCollection = new Db(new MongoClient(''), '').collection('memes'); expectType( + await memeCollection + .find({ _id: { $in: [] } }) + .sort({ _id: -1 }) + .limit(3) + .project(publicMemeProjection) // <== Argument of type T is not assignable to parameter of type U + .toArray() +); + +// Returns you're untouched schema when no override given +expectType( + await memeCollection + .find({ _id: { $in: [] } }) + .sort({ _id: -1 }) + .limit(3) + .project(publicMemeProjection) // <== Argument of type T is not assignable to parameter of type U + .toArray() +); + +// Returns generic document when there is no schema +expectType( await new Db(new MongoClient(''), '') - .collection('memes') + .collection('memes') .find({ _id: { $in: [] } }) .sort({ _id: -1 }) .limit(3) - .project(publicMemeProjection) // <== location of TS2345 error + .project(publicMemeProjection) // <== Argument of type T is not assignable to parameter of type U .toArray() ); From d02193f57fb7c4fbcfeff6cca4478f23cb95b43c Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 3 Aug 2021 13:26:01 -0400 Subject: [PATCH 5/9] fix: lint --- src/operations/find.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/operations/find.ts b/src/operations/find.ts index b29bdb4de1..1fb9e5630c 100644 --- a/src/operations/find.ts +++ b/src/operations/find.ts @@ -18,6 +18,7 @@ import type { ClientSession } from '../sessions'; import type { Projection } from '../mongo_types'; /** @public */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars export interface FindOptions extends CommandOperationOptions { /** Sets the limit of documents returned in the query. */ limit?: number; From 8c5f29f10067374284620642d4a03574bf1e17fe Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 3 Aug 2021 13:42:00 -0400 Subject: [PATCH 6/9] fix: deprecate the projection type --- src/mongo_types.ts | 12 ++++++++++-- src/operations/find.ts | 3 +-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/mongo_types.ts b/src/mongo_types.ts index 4abff4e53e..4a80332773 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -169,10 +169,18 @@ export type BSONType = typeof BSONType[keyof typeof BSONType]; /** @public */ export type BSONTypeAlias = keyof typeof BSONType; -/** @public Projection is flexible to permit the wide array of aggregation operators */ +/** + * @public + * Projection is flexible to permit the wide array of aggregation operators + * @deprecated since v4.1.0: Since projections support all of aggregation operations we have no plans to narrow this type further + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export type Projection = Document; -/** @public */ + +/** + * @public + * @deprecated since v4.1.0: Since projections support all of aggregation operations we have no plans to narrow this type further + */ export type ProjectionOperators = Document; /** @public */ diff --git a/src/operations/find.ts b/src/operations/find.ts index 1fb9e5630c..2541df9f5e 100644 --- a/src/operations/find.ts +++ b/src/operations/find.ts @@ -15,7 +15,6 @@ import { Sort, formatSort } from '../sort'; import { isSharded } from '../cmap/wire_protocol/shared'; import { ReadConcern } from '../read_concern'; import type { ClientSession } from '../sessions'; -import type { Projection } from '../mongo_types'; /** @public */ // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -25,7 +24,7 @@ export interface FindOptions extends Comman /** Set to sort the documents coming back from the query. Array of indexes, `[['a', 1]]` etc. */ sort?: Sort; /** The fields to return in the query. Object of fields to either include or exclude (one of, not both), `{'a':1, 'b': 1}` **or** `{'a': 0, 'b': 0}` */ - projection?: Projection; + projection?: Document; /** Set to skip N documents ahead in your query (useful for pagination). */ skip?: number; /** Tell the query to use specific indexes in the query. Object of indexes to use, `{'_id':1}` */ From d8b55b4aecf43def33a8b84a246800cc44f5a638 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 3 Aug 2021 14:11:50 -0400 Subject: [PATCH 7/9] fix: cursor returns generic doc --- src/cursor/aggregation_cursor.ts | 2 +- src/cursor/find_cursor.ts | 2 +- src/operations/find.ts | 5 ++++- test/types/community/collection/findX.test-d.ts | 3 ++- test/types/community/cursor.test-d.ts | 15 +++++---------- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/cursor/aggregation_cursor.ts b/src/cursor/aggregation_cursor.ts index 8d6bfab39e..a7c20381c9 100644 --- a/src/cursor/aggregation_cursor.ts +++ b/src/cursor/aggregation_cursor.ts @@ -152,7 +152,7 @@ export class AggregationCursor extends AbstractCursor($project: Document): AggregationCursor; + project($project: Document): 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 d7e88393bb..2cf904e744 100644 --- a/src/cursor/find_cursor.ts +++ b/src/cursor/find_cursor.ts @@ -361,7 +361,7 @@ export class FindCursor extends AbstractCursor { * ); * ``` */ - project(value: Document): FindCursor; + project(value: Document): FindCursor; project(value: Document): this { assertUninitialized(this); this[kBuiltOptions].projection = value; diff --git a/src/operations/find.ts b/src/operations/find.ts index 2541df9f5e..3b1b9c239e 100644 --- a/src/operations/find.ts +++ b/src/operations/find.ts @@ -16,7 +16,10 @@ import { isSharded } from '../cmap/wire_protocol/shared'; import { ReadConcern } from '../read_concern'; import type { ClientSession } from '../sessions'; -/** @public */ +/** + * @public + * @typeParam TSchema - Unused schema definition, deprecated usage, only specify `FindOptions` with no generic + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export interface FindOptions extends CommandOperationOptions { /** Sets the limit of documents returned in the query. */ diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 06ee9b95d0..16c596807d 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -128,7 +128,8 @@ const optionsWithProjection: FindOptions = { } }; -expectNotType({ +// this is changed in NODE-3454 to be the opposite test since Projection is flexible now +expectAssignable({ projection: { make: 'invalid' } diff --git a/test/types/community/cursor.test-d.ts b/test/types/community/cursor.test-d.ts index aa5d6d1363..48e3f5a627 100644 --- a/test/types/community/cursor.test-d.ts +++ b/test/types/community/cursor.test-d.ts @@ -22,7 +22,7 @@ const cursor = collection .min({ age: 18 }) .maxAwaitTimeMS(1) .maxTimeMS(1) - .project({}) + // .project({}) -> projections removes the types from the returned documents .returnKey(true) .showRecordId(true) .skip(1) @@ -31,6 +31,7 @@ const cursor = collection expectType>(cursor); expectType(cursor.stream()); +expectType>(cursor.project({})); collection.find().project({}); collection.find().project({ notExistingField: 1 }); @@ -118,7 +119,7 @@ interface PublicMeme { likes: number; someRandomProp: boolean; // Projection makes no enforcement on anything // the convenience parameter project() allows you to define a return type, - // otherwise projections returns your schema + // otherwise projections returns generic document } const publicMemeProjection = { @@ -132,18 +133,14 @@ const memeCollection = new Db(new MongoClient(''), '').collection( expectType( await memeCollection .find({ _id: { $in: [] } }) - .sort({ _id: -1 }) - .limit(3) .project(publicMemeProjection) // <== Argument of type T is not assignable to parameter of type U .toArray() ); -// Returns you're untouched schema when no override given -expectType( +// Returns generic document when no override given +expectNotType( await memeCollection .find({ _id: { $in: [] } }) - .sort({ _id: -1 }) - .limit(3) .project(publicMemeProjection) // <== Argument of type T is not assignable to parameter of type U .toArray() ); @@ -153,8 +150,6 @@ expectType( await new Db(new MongoClient(''), '') .collection('memes') .find({ _id: { $in: [] } }) - .sort({ _id: -1 }) - .limit(3) .project(publicMemeProjection) // <== Argument of type T is not assignable to parameter of type U .toArray() ); From 0e1ca61671171729124ee2118d717cb69d3ccdec Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 4 Aug 2021 13:58:01 -0400 Subject: [PATCH 8/9] fix: don't use overrrides for project --- src/cursor/aggregation_cursor.ts | 32 +++++++++++++++++++++++--------- src/cursor/find_cursor.ts | 31 ++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/cursor/aggregation_cursor.ts b/src/cursor/aggregation_cursor.ts index a7c20381c9..4afff532b9 100644 --- a/src/cursor/aggregation_cursor.ts +++ b/src/cursor/aggregation_cursor.ts @@ -134,29 +134,43 @@ export class AggregationCursor extends AbstractCursor = cursor.project<{ a: number }>({ _id: 0, a: true }); + * // Flexible way + * const docs: AggregationCursor = cursor.project({ _id: 0, a: true }); + * ``` + * + * @remarks + * In order to strictly type this function you must provide an interface + * that represents the effect of your projection on the result documents. + * + * 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 projectCursor = cursor.project<{ a: number }>({ _id: 0, a: true }); * const aPropOnlyArray: {a: number}[] = await projectCursor.toArray(); * * // or always use chaining and save the final cursor * - * const cursor = coll.aggregate<{ a: number }>() - * .project<{ a: string }>( - * { a: { $convert: { input: '$a' to: 'string' } }} - * ); + * const cursor = coll.aggregate().project<{ a: string }>({ + * _id: 0, + * a: { $convert: { input: '$a', to: 'string' } + * }}); * ``` */ - project($project: Document): AggregationCursor; - project($project: Document): this { + project($project: Document): AggregationCursor { assertUninitialized(this); this[kPipeline].push({ $project }); - return this; + return (this as unknown) as AggregationCursor; } /** Add a lookup stage to the aggregation pipeline */ diff --git a/src/cursor/find_cursor.ts b/src/cursor/find_cursor.ts index 2cf904e744..a2a88fa330 100644 --- a/src/cursor/find_cursor.ts +++ b/src/cursor/find_cursor.ts @@ -343,29 +343,42 @@ export class FindCursor extends AbstractCursor { * 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, + * By default chaining a projection to your cursor changes the returned type to the generic + * {@link Document} type. + * You should specify a parameterized type to have assertions on your final results. + * + * @example + * ```typescript + * // Best way + * const docs: FindCursor<{ a: number }> = cursor.project<{ a: number }>({ _id: 0, a: true }); + * // Flexible way + * const docs: FindCursor = cursor.project({ _id: 0, a: true }); + * ``` + * + * @remarks + * + * 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 projectCursor = cursor.project<{ a: number }>({ _id: 0, a: true }); * const aPropOnlyArray: {a: number}[] = await projectCursor.toArray(); * * // or always use chaining and save the final cursor * - * const cursor = coll.find() - * .project<{ a: string }>( - * { a: { $convert: { input: '$a' to: 'string' } }} - * ); + * const cursor = coll.find().project<{ a: string }>({ + * _id: 0, + * a: { $convert: { input: '$a', to: 'string' } + * }}); * ``` */ - project(value: Document): FindCursor; - project(value: Document): this { + project(value: Document): FindCursor { assertUninitialized(this); this[kBuiltOptions].projection = value; - return this; + return (this as unknown) as FindCursor; } /** From 48aa9914c61788b37f6e9947ae4c8d9e2165d61f Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 4 Aug 2021 14:50:53 -0400 Subject: [PATCH 9/9] fix: comments --- src/mongo_types.ts | 4 ++-- test/types/community/cursor.test-d.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mongo_types.ts b/src/mongo_types.ts index 4a80332773..d4107e1418 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -172,14 +172,14 @@ export type BSONTypeAlias = keyof typeof BSONType; /** * @public * Projection is flexible to permit the wide array of aggregation operators - * @deprecated since v4.1.0: Since projections support all of aggregation operations we have no plans to narrow this type further + * @deprecated since v4.1.0: Since projections support all aggregation operations we have no plans to narrow this type further */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export type Projection = Document; /** * @public - * @deprecated since v4.1.0: Since projections support all of aggregation operations we have no plans to narrow this type further + * @deprecated since v4.1.0: Since projections support all aggregation operations we have no plans to narrow this type further */ export type ProjectionOperators = Document; diff --git a/test/types/community/cursor.test-d.ts b/test/types/community/cursor.test-d.ts index 48e3f5a627..503d70d408 100644 --- a/test/types/community/cursor.test-d.ts +++ b/test/types/community/cursor.test-d.ts @@ -126,14 +126,14 @@ const publicMemeProjection = { myId: { $toString: '$_id' }, owner: { $toString: '$owner' }, receiver: { $toString: '$receiver' }, - likes: '$totalLikes' // <== cause of TS2345 error + likes: '$totalLikes' // <== (NODE-3454) cause of TS2345 error: Argument of type T is not assignable to parameter of type U }; const memeCollection = new Db(new MongoClient(''), '').collection('memes'); expectType( await memeCollection .find({ _id: { $in: [] } }) - .project(publicMemeProjection) // <== Argument of type T is not assignable to parameter of type U + .project(publicMemeProjection) // <== .toArray() ); @@ -141,7 +141,7 @@ expectType( expectNotType( await memeCollection .find({ _id: { $in: [] } }) - .project(publicMemeProjection) // <== Argument of type T is not assignable to parameter of type U + .project(publicMemeProjection) .toArray() ); @@ -150,6 +150,6 @@ expectType( await new Db(new MongoClient(''), '') .collection('memes') .find({ _id: { $in: [] } }) - .project(publicMemeProjection) // <== Argument of type T is not assignable to parameter of type U + .project(publicMemeProjection) .toArray() );