Skip to content

Commit 74bd7bd

Browse files
authored
fix(NODE-3468): remove generic overrides from find (#2935)
1 parent 0dc9b45 commit 74bd7bd

16 files changed

+147
-84
lines changed

package.json

+9-2
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,9 @@
107107
"build:docs": "typedoc",
108108
"check:bench": "node test/benchmarks/driverBench",
109109
"check:coverage": "nyc npm run check:test",
110-
"check:lint": "npm run build:dts && npm run check:dts && npm run check:eslint",
110+
"check:lint": "npm run build:dts && npm run check:dts && npm run check:eslint && npm run check:tsd",
111111
"check:eslint": "eslint -v && eslint --max-warnings=0 --ext '.js,.ts' src test",
112+
"check:tsd": "tsd --version && tsd",
112113
"check:dts": "tsc --noEmit mongodb.d.ts && tsd",
113114
"check:test": "mocha --recursive test/functional test/unit",
114115
"check:ts": "tsc -v && tsc --noEmit",
@@ -124,6 +125,12 @@
124125
"test": "npm run check:lint && npm run check:test"
125126
},
126127
"tsd": {
127-
"directory": "test/types"
128+
"directory": "test/types",
129+
"compilerOptions": {
130+
"strict": true,
131+
"target": "esnext",
132+
"module": "commonjs",
133+
"moduleResolution": "node"
134+
}
128135
}
129136
}

src/collection.ts

+17-12
Original file line numberDiff line numberDiff line change
@@ -414,34 +414,39 @@ export class Collection<TSchema extends Document = Document> {
414414
updateOne(
415415
filter: Filter<TSchema>,
416416
update: UpdateFilter<TSchema> | Partial<TSchema>
417-
): Promise<UpdateResult | Document>;
417+
): Promise<UpdateResult>;
418418
updateOne(
419419
filter: Filter<TSchema>,
420420
update: UpdateFilter<TSchema> | Partial<TSchema>,
421-
callback: Callback<UpdateResult | Document>
421+
callback: Callback<UpdateResult>
422422
): void;
423423
updateOne(
424424
filter: Filter<TSchema>,
425425
update: UpdateFilter<TSchema> | Partial<TSchema>,
426426
options: UpdateOptions
427-
): Promise<UpdateResult | Document>;
427+
): Promise<UpdateResult>;
428428
updateOne(
429429
filter: Filter<TSchema>,
430430
update: UpdateFilter<TSchema> | Partial<TSchema>,
431431
options: UpdateOptions,
432-
callback: Callback<UpdateResult | Document>
432+
callback: Callback<UpdateResult>
433433
): void;
434434
updateOne(
435435
filter: Filter<TSchema>,
436436
update: UpdateFilter<TSchema> | Partial<TSchema>,
437-
options?: UpdateOptions | Callback<UpdateResult | Document>,
438-
callback?: Callback<UpdateResult | Document>
439-
): Promise<UpdateResult | Document> | void {
437+
options?: UpdateOptions | Callback<UpdateResult>,
438+
callback?: Callback<UpdateResult>
439+
): Promise<UpdateResult> | void {
440440
if (typeof options === 'function') (callback = options), (options = {});
441441

442442
return executeOperation(
443443
getTopology(this),
444-
new UpdateOneOperation(this as TODO_NODE_3286, filter, update, resolveOptions(this, options)),
444+
new UpdateOneOperation(
445+
this as TODO_NODE_3286,
446+
filter,
447+
update,
448+
resolveOptions(this, options)
449+
) as TODO_NODE_3286,
445450
callback
446451
);
447452
}
@@ -685,10 +690,10 @@ export class Collection<TSchema extends Document = Document> {
685690
// allow an override of the schema.
686691
findOne<T = TSchema>(): Promise<T | undefined>;
687692
findOne<T = TSchema>(callback: Callback<T | undefined>): void;
688-
findOne<T = TSchema>(filter: Filter<T>): Promise<T | undefined>;
689-
findOne<T = TSchema>(filter: Filter<T>, options?: FindOptions): Promise<T | undefined>;
693+
findOne<T = TSchema>(filter: Filter<TSchema>): Promise<T | undefined>;
694+
findOne<T = TSchema>(filter: Filter<TSchema>, options?: FindOptions): Promise<T | undefined>;
690695
findOne<T = TSchema>(
691-
filter: Filter<T>,
696+
filter: Filter<TSchema>,
692697
options?: FindOptions,
693698
callback?: Callback<T | undefined>
694699
): void;
@@ -725,7 +730,7 @@ export class Collection<TSchema extends Document = Document> {
725730
*/
726731
find(): FindCursor<TSchema>;
727732
find(filter: Filter<TSchema>, options?: FindOptions): FindCursor<TSchema>;
728-
find<T = TSchema>(filter: Filter<T>, options?: FindOptions): FindCursor<T>;
733+
find<T>(filter: Filter<TSchema>, options?: FindOptions): FindCursor<T>;
729734
find(filter?: Filter<TSchema>, options?: FindOptions): FindCursor<TSchema> {
730735
if (arguments.length > 2) {
731736
throw new MongoInvalidArgumentError(

src/cursor/abstract_cursor.ts

+24-24
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,7 @@ export abstract class AbstractCursor<
280280
return done(undefined, true);
281281
}
282282

283-
next<any>(this, true, (err, doc) => {
284-
// FIXME(NODE):
283+
next<TSchema>(this, true, (err, doc) => {
285284
if (err) return done(err);
286285

287286
if (doc) {
@@ -296,9 +295,9 @@ export abstract class AbstractCursor<
296295
}
297296

298297
/** Get the next available document from the cursor, returns null if no more documents are available. */
299-
next<T = TSchema>(): Promise<T | null>;
300-
next<T = TSchema>(callback: Callback<T | null>): void;
301-
next<T = TSchema>(callback?: Callback<T | null>): Promise<T | null> | void {
298+
next(): Promise<TSchema | null>;
299+
next(callback: Callback<TSchema | null>): void;
300+
next(callback?: Callback<TSchema | null>): Promise<TSchema | null> | void {
302301
return maybePromise(callback, done => {
303302
if (this[kId] === Long.ZERO) {
304303
return done(new MongoCursorExhaustedError());
@@ -311,9 +310,9 @@ export abstract class AbstractCursor<
311310
/**
312311
* Try to get the next available document from the cursor or `null` if an empty batch is returned
313312
*/
314-
tryNext<T = TSchema>(): Promise<T | null>;
315-
tryNext<T = TSchema>(callback: Callback<T | null>): void;
316-
tryNext<T = TSchema>(callback?: Callback<T | null>): Promise<T | null> | void {
313+
tryNext(): Promise<TSchema | null>;
314+
tryNext(callback: Callback<TSchema | null>): void;
315+
tryNext(callback?: Callback<TSchema | null>): Promise<TSchema | null> | void {
317316
return maybePromise(callback, done => {
318317
if (this[kId] === Long.ZERO) {
319318
return done(new MongoCursorExhaustedError());
@@ -329,10 +328,10 @@ export abstract class AbstractCursor<
329328
* @param iterator - The iteration callback.
330329
* @param callback - The end callback.
331330
*/
332-
forEach<T = TSchema>(iterator: (doc: T) => boolean | void): Promise<void>;
333-
forEach<T = TSchema>(iterator: (doc: T) => boolean | void, callback: Callback<void>): void;
334-
forEach<T = TSchema>(
335-
iterator: (doc: T) => boolean | void,
331+
forEach(iterator: (doc: TSchema) => boolean | void): Promise<void>;
332+
forEach(iterator: (doc: TSchema) => boolean | void, callback: Callback<void>): void;
333+
forEach(
334+
iterator: (doc: TSchema) => boolean | void,
336335
callback?: Callback<void>
337336
): Promise<void> | void {
338337
if (typeof iterator !== 'function') {
@@ -341,7 +340,7 @@ export abstract class AbstractCursor<
341340
return maybePromise(callback, done => {
342341
const transform = this[kTransform];
343342
const fetchDocs = () => {
344-
next<T>(this, true, (err, doc) => {
343+
next<TSchema>(this, true, (err, doc) => {
345344
if (err || doc == null) return done(err);
346345
let result;
347346
// NOTE: no need to transform because `next` will do this automatically
@@ -358,7 +357,7 @@ export abstract class AbstractCursor<
358357
for (let i = 0; i < internalDocs.length; ++i) {
359358
try {
360359
result = iterator(
361-
(transform ? transform(internalDocs[i]) : internalDocs[i]) as T // TODO(NODE-3283): Improve transform typing
360+
(transform ? transform(internalDocs[i]) : internalDocs[i]) as TSchema // TODO(NODE-3283): Improve transform typing
362361
);
363362
} catch (error) {
364363
return done(error);
@@ -402,15 +401,15 @@ export abstract class AbstractCursor<
402401
*
403402
* @param callback - The result callback.
404403
*/
405-
toArray<T = TSchema>(): Promise<T[]>;
406-
toArray<T = TSchema>(callback: Callback<T[]>): void;
407-
toArray<T = TSchema>(callback?: Callback<T[]>): Promise<T[]> | void {
404+
toArray(): Promise<TSchema[]>;
405+
toArray(callback: Callback<TSchema[]>): void;
406+
toArray(callback?: Callback<TSchema[]>): Promise<TSchema[]> | void {
408407
return maybePromise(callback, done => {
409-
const docs: T[] = [];
408+
const docs: TSchema[] = [];
410409
const transform = this[kTransform];
411410
const fetchDocs = () => {
412411
// NOTE: if we add a `nextBatch` then we should use it here
413-
next<T>(this, true, (err, doc) => {
412+
next<TSchema>(this, true, (err, doc) => {
414413
if (err) return done(err);
415414
if (doc == null) return done(undefined, docs);
416415

@@ -420,7 +419,7 @@ export abstract class AbstractCursor<
420419
// these do need to be transformed since they are copying the rest of the batch
421420
const internalDocs = (transform
422421
? this[kDocuments].splice(0, this[kDocuments].length).map(transform)
423-
: this[kDocuments].splice(0, this[kDocuments].length)) as T[]; // TODO(NODE-3283): Improve transform typing
422+
: this[kDocuments].splice(0, this[kDocuments].length)) as TSchema[]; // TODO(NODE-3283): Improve transform typing
424423

425424
if (internalDocs) {
426425
docs.push(...internalDocs);
@@ -458,11 +457,12 @@ export abstract class AbstractCursor<
458457
* Map all documents using the provided function
459458
* If there is a transform set on the cursor, that will be called first and the result passed to
460459
* this function's transform.
461-
* @remarks
462460
*
463-
* **NOTE:** adding a transform changes the return type of the iteration of this cursor, it **does not** return
464-
* a new instance of a cursor. This means when calling map, you should always assign the result to a new
465-
* variable. Take note of the following example:
461+
* @remarks
462+
* **Note for Typescript Users:** adding a transform changes the return type of the iteration of this cursor,
463+
* it **does not** return a new instance of a cursor. This means when calling map,
464+
* you should always assign the result to a new variable in order to get a correctly typed cursor variable.
465+
* Take note of the following example:
466466
*
467467
* @example
468468
* ```typescript

src/cursor/aggregation_cursor.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,10 @@ export class AggregationCursor<TSchema = Document> extends AbstractCursor<TSchem
149149
* In order to strictly type this function you must provide an interface
150150
* that represents the effect of your projection on the result documents.
151151
*
152-
* Adding a projection changes the return type of the iteration of this cursor,
152+
* **Note for Typescript Users:** adding a transform changes the return type of the iteration of this cursor,
153153
* it **does not** return a new instance of a cursor. This means when calling project,
154-
* you should always assign the result to a new variable. Take note of the following example:
154+
* you should always assign the result to a new variable in order to get a correctly typed cursor variable.
155+
* Take note of the following example:
155156
*
156157
* @example
157158
* ```typescript

src/cursor/find_cursor.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,10 @@ export class FindCursor<TSchema = Document> extends AbstractCursor<TSchema> {
357357
*
358358
* @remarks
359359
*
360-
* Adding a projection changes the return type of the iteration of this cursor,
360+
* **Note for Typescript Users:** adding a transform changes the return type of the iteration of this cursor,
361361
* it **does not** return a new instance of a cursor. This means when calling project,
362-
* you should always assign the result to a new variable. Take note of the following example:
362+
* you should always assign the result to a new variable in order to get a correctly typed cursor variable.
363+
* Take note of the following example:
363364
*
364365
* @example
365366
* ```typescript

src/db.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,10 @@ export class Db {
289289
* @param pipeline - An array of aggregation stages to be executed
290290
* @param options - Optional settings for the command
291291
*/
292-
aggregate(pipeline: Document[] = [], options?: AggregateOptions): AggregationCursor {
292+
aggregate<T = Document>(
293+
pipeline: Document[] = [],
294+
options?: AggregateOptions
295+
): AggregationCursor<T> {
293296
if (arguments.length > 2) {
294297
throw new MongoInvalidArgumentError('Method "db.aggregate()" accepts at most two arguments');
295298
}

src/error.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ export interface ErrorDescription extends Document {
7878
export class MongoError extends Error {
7979
/** @internal */
8080
[kErrorLabels]: Set<string>;
81+
/**
82+
* This is a number in MongoServerError and a string in MongoDriverError
83+
* @privateRemarks
84+
* Define the type override on the subclasses when we can use the override keyword
85+
*/
8186
code?: number | string;
8287
topologyVersion?: TopologyVersion;
8388

@@ -132,12 +137,10 @@ export class MongoError extends Error {
132137
* @category Error
133138
*/
134139
export class MongoServerError extends MongoError {
135-
code?: number;
136140
codeName?: string;
137141
writeConcernError?: Document;
138142
errInfo?: Document;
139143
ok?: number;
140-
topologyVersion?: TopologyVersion;
141144
[key: string]: any;
142145

143146
constructor(message: ErrorDescription) {
@@ -164,7 +167,6 @@ export class MongoServerError extends MongoError {
164167
* @category Error
165168
*/
166169
export class MongoDriverError extends MongoError {
167-
code?: string;
168170
constructor(message: string) {
169171
super(message);
170172
}

src/operations/list_collections.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ export interface CollectionInfo extends Document {
115115

116116
/** @public */
117117
export class ListCollectionsCursor<
118-
T extends Pick<CollectionInfo, 'name' | 'type'> | CollectionInfo = CollectionInfo
118+
T extends Pick<CollectionInfo, 'name' | 'type'> | CollectionInfo =
119+
| Pick<CollectionInfo, 'name' | 'type'>
120+
| CollectionInfo
119121
> extends AbstractCursor<T> {
120122
parent: Db;
121123
filter: Document;

test/types/.eslintrc.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"prettier/prettier": "error",
2525
"tsdoc/syntax": "warn",
2626
"@typescript-eslint/no-explicit-any": "off",
27-
"@typescript-eslint/no-unused-vars": "error"
27+
"@typescript-eslint/no-unused-vars": "error",
28+
"@typescript-eslint/ban-ts-comment": "off",
29+
"@typescript-eslint/no-empty-function":"off"
2830
}
2931
}

test/types/community/collection/filterQuery.test-d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const collectionT = db.collection<PetModel>('test.filterQuery');
5050
// Assert that collection.find uses the Filter helper like so:
5151
const filter: Filter<PetModel> = {};
5252
collectionT.find(filter);
53+
collectionT.find(spot); // a whole model definition is also a valid filter
5354
// Now tests below can directly test the Filter helper, and are implicitly checking collection.find
5455

5556
/**
@@ -226,3 +227,7 @@ await collectionT.find({ playmates: { $elemMatch: { name: 'MrMeow' } } }).toArra
226227
expectNotType<Filter<PetModel>>({ name: { $all: ['world', 'world'] } });
227228
expectNotType<Filter<PetModel>>({ age: { $elemMatch: [1, 2] } });
228229
expectNotType<Filter<PetModel>>({ type: { $size: 2 } });
230+
231+
// dot key case that shows it is assignable even when the referenced key is the wrong type
232+
expectAssignable<Filter<PetModel>>({ 'bestFriend.name': 23 }); // using dot notation permits any type for the key
233+
expectNotType<Filter<PetModel>>({ bestFriend: { name: 23 } });

test/types/community/collection/findX.test-d.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ expectType<{ cost: number } | undefined>(
102102
await collectionBag.findOne<{ cost: number }>({ color: 'red' }, { projection: { cost: 1 } })
103103
);
104104

105+
// NODE-3468 The generic in find and findOne no longer affect the filter type
106+
type Pet = { type: string; age: number };
107+
const pets = db.collection<Pet>('pets');
108+
109+
expectType<{ crazy: number }[]>(
110+
await pets
111+
.find<{ crazy: number }>({ type: 'dog', age: 1 })
112+
.toArray()
113+
);
114+
105115
interface Car {
106116
make: string;
107117
}
@@ -186,12 +196,16 @@ expectNotType<FindCursor<{ color: string | { $in: ReadonlyArray<string> } }>>(
186196
colorCollection.find({ color: { $in: colorsFreeze } })
187197
);
188198

189-
// This is related to another bug that will be fixed in NODE-3454
190-
expectType<FindCursor<{ color: { $in: number } }>>(colorCollection.find({ color: { $in: 3 } }));
199+
// NODE-3454: Using the incorrect $in value doesn't mess with the resulting schema
200+
expectNotType<FindCursor<{ color: { $in: number } }>>(
201+
colorCollection.find({ color: { $in: 3 as any } }) // `as any` is to let us make this mistake and still show the result type isn't broken
202+
);
203+
expectType<FindCursor<{ color: string }>>(colorCollection.find({ color: { $in: 3 as any } }));
191204

192205
// When you use the override, $in doesn't permit readonly
193206
colorCollection.find<{ color: string }>({ color: { $in: colorsFreeze } });
194207
colorCollection.find<{ color: string }>({ color: { $in: ['regularArray'] } });
208+
195209
// This is a regression test that we don't remove the unused generic in FindOptions
196210
const findOptions: FindOptions<{ a: number }> = {};
197211
expectType<FindOptions>(findOptions);

0 commit comments

Comments
 (0)