Skip to content

Commit 52520aa

Browse files
authored
feat(NODE-3729): add withId to default return type for collection.find and collection.findOne (#3039)
1 parent 1f8b539 commit 52520aa

File tree

5 files changed

+113
-52
lines changed

5 files changed

+113
-52
lines changed

src/collection.ts

+20-16
Original file line numberDiff line numberDiff line change
@@ -676,12 +676,16 @@ export class Collection<TSchema extends Document = Document> {
676676
* @param options - Optional settings for the command
677677
* @param callback - An optional callback, a Promise will be returned if none is provided
678678
*/
679-
findOne(): Promise<TSchema | null>;
680-
findOne(callback: Callback<TSchema | null>): void;
681-
findOne(filter: Filter<TSchema>): Promise<TSchema | null>;
682-
findOne(filter: Filter<TSchema>, callback: Callback<TSchema | null>): void;
683-
findOne(filter: Filter<TSchema>, options: FindOptions): Promise<TSchema | null>;
684-
findOne(filter: Filter<TSchema>, options: FindOptions, callback: Callback<TSchema | null>): void;
679+
findOne(): Promise<WithId<TSchema> | null>;
680+
findOne(callback: Callback<WithId<TSchema> | null>): void;
681+
findOne(filter: Filter<TSchema>): Promise<WithId<TSchema> | null>;
682+
findOne(filter: Filter<TSchema>, callback: Callback<WithId<TSchema> | null>): void;
683+
findOne(filter: Filter<TSchema>, options: FindOptions): Promise<WithId<TSchema> | null>;
684+
findOne(
685+
filter: Filter<TSchema>,
686+
options: FindOptions,
687+
callback: Callback<WithId<TSchema> | null>
688+
): void;
685689

686690
// allow an override of the schema.
687691
findOne<T = TSchema>(): Promise<T | null>;
@@ -695,18 +699,18 @@ export class Collection<TSchema extends Document = Document> {
695699
): void;
696700

697701
findOne(
698-
filter?: Filter<TSchema> | Callback<TSchema | null>,
699-
options?: FindOptions | Callback<TSchema | null>,
700-
callback?: Callback<TSchema | null>
701-
): Promise<TSchema | null> | void {
702+
filter?: Filter<TSchema> | Callback<WithId<TSchema> | null>,
703+
options?: FindOptions | Callback<WithId<TSchema> | null>,
704+
callback?: Callback<WithId<TSchema> | null>
705+
): Promise<WithId<TSchema> | null> | void {
702706
if (callback != null && typeof callback !== 'function') {
703707
throw new MongoInvalidArgumentError(
704708
'Third parameter to `findOne()` must be a callback or undefined'
705709
);
706710
}
707711

708712
if (typeof filter === 'function') {
709-
callback = filter as Callback<TSchema | null>;
713+
callback = filter as Callback<WithId<TSchema> | null>;
710714
filter = {};
711715
options = {};
712716
}
@@ -725,10 +729,10 @@ export class Collection<TSchema extends Document = Document> {
725729
*
726730
* @param filter - The filter predicate. If unspecified, then all documents in the collection will match the predicate
727731
*/
728-
find(): FindCursor<TSchema>;
729-
find(filter: Filter<TSchema>, options?: FindOptions): FindCursor<TSchema>;
730-
find<T>(filter: Filter<TSchema>, options?: FindOptions): FindCursor<T>;
731-
find(filter?: Filter<TSchema>, options?: FindOptions): FindCursor<TSchema> {
732+
find(): FindCursor<WithId<TSchema>>;
733+
find(filter: Filter<WithId<TSchema>>, options?: FindOptions): FindCursor<WithId<TSchema>>;
734+
find<T>(filter: Filter<WithId<TSchema>>, options?: FindOptions): FindCursor<T>;
735+
find(filter?: Filter<WithId<TSchema>>, options?: FindOptions): FindCursor<WithId<TSchema>> {
732736
if (arguments.length > 2) {
733737
throw new MongoInvalidArgumentError(
734738
'Method "collection.find()" accepts at most two arguments'
@@ -738,7 +742,7 @@ export class Collection<TSchema extends Document = Document> {
738742
throw new MongoInvalidArgumentError('Argument "options" must not be function');
739743
}
740744

741-
return new FindCursor<TSchema>(
745+
return new FindCursor<WithId<TSchema>>(
742746
getTopology(this),
743747
this.s.namespace,
744748
filter,

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

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BSONRegExp, Decimal128, ObjectId } from 'bson';
22
import { expectAssignable, expectNotType, expectType } from 'tsd';
3-
import { Filter, MongoClient } from '../../../../src';
3+
import { Filter, MongoClient, WithId } from '../../../../src';
44

55
/**
66
* test the Filter type using collection.find<T>() method
@@ -57,12 +57,16 @@ collectionT.find(spot); // a whole model definition is also a valid filter
5757
* test simple field queries e.g. `{ name: 'Spot' }`
5858
*/
5959
/// it should query __string__ fields
60-
expectType<PetModel[]>(await collectionT.find({ name: 'Spot' }).toArray());
60+
expectType<WithId<PetModel>[]>(await collectionT.find({ name: 'Spot' }).toArray());
6161
// it should query string fields by regex
62-
expectType<PetModel[]>(await collectionT.find({ name: /Blu/i }).toArray());
62+
expectType<WithId<PetModel>[]>(await collectionT.find({ name: /Blu/i }).toArray());
6363
// it should query string fields by RegExp object, and bson regex
64-
expectType<PetModel[]>(await collectionT.find({ name: new RegExp('MrMeow', 'i') }).toArray());
65-
expectType<PetModel[]>(await collectionT.find({ name: new BSONRegExp('MrMeow', 'i') }).toArray());
64+
expectType<WithId<PetModel>[]>(
65+
await collectionT.find({ name: new RegExp('MrMeow', 'i') }).toArray()
66+
);
67+
expectType<WithId<PetModel>[]>(
68+
await collectionT.find({ name: new BSONRegExp('MrMeow', 'i') }).toArray()
69+
);
6670
/// it should not accept wrong types for string fields
6771
expectNotType<Filter<PetModel>>({ name: 23 });
6872
expectNotType<Filter<PetModel>>({ name: { suffix: 'Jr' } });
@@ -90,9 +94,9 @@ expectNotType<Filter<PetModel>>({ bestFriend: [{ family: 'Andersons' }] });
9094
/// it should query __array__ fields by exact match
9195
await collectionT.find({ treats: ['kibble', 'bone'] }).toArray();
9296
/// it should query __array__ fields by element type
93-
expectType<PetModel[]>(await collectionT.find({ treats: 'kibble' }).toArray());
94-
expectType<PetModel[]>(await collectionT.find({ treats: /kibble/i }).toArray());
95-
expectType<PetModel[]>(await collectionT.find({ friends: spot }).toArray());
97+
expectType<WithId<PetModel>[]>(await collectionT.find({ treats: 'kibble' }).toArray());
98+
expectType<WithId<PetModel>[]>(await collectionT.find({ treats: /kibble/i }).toArray());
99+
expectType<WithId<PetModel>[]>(await collectionT.find({ friends: spot }).toArray());
96100
/// it should not query array fields by wrong types
97101
expectNotType<Filter<PetModel>>({ treats: 12 });
98102
expectNotType<Filter<PetModel>>({ friends: { name: 'not a full model' } });
@@ -206,7 +210,7 @@ await collectionT.find({ $where: 'function() { return true }' }).toArray();
206210
await collectionT
207211
.find({
208212
$where: function () {
209-
expectType<PetModel>(this);
213+
expectType<WithId<PetModel>>(this);
210214
return this.name === 'MrMeow';
211215
}
212216
})

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

+76-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { expectAssignable, expectNotType, expectType } from 'tsd';
2-
import { FindCursor, FindOptions, MongoClient, Document, Collection, Db } from '../../../../src';
2+
import {
3+
FindCursor,
4+
FindOptions,
5+
MongoClient,
6+
Document,
7+
Collection,
8+
Db,
9+
WithId,
10+
ObjectId
11+
} from '../../../../src';
312
import type { Projection, ProjectionOperators } from '../../../../src';
413
import type { PropExists } from '../../utility_types';
514

@@ -10,7 +19,11 @@ const collection = db.collection('test.find');
1019

1120
// Locate all the entries using find
1221
collection.find({}).toArray((_err, fields) => {
13-
expectType<Document[] | undefined>(fields);
22+
expectType<WithId<Document>[] | undefined>(fields);
23+
if (fields) {
24+
expectType<ObjectId>(fields[0]._id);
25+
expectNotType<ObjectId | undefined>(fields[0]._id);
26+
}
1427
});
1528

1629
// test with collection type
@@ -26,7 +39,7 @@ collectionT.find({
2639
$and: [{ numberField: { $gt: 0 } }, { numberField: { $lt: 100 } }],
2740
readonlyFruitTags: { $all: ['apple', 'pear'] }
2841
});
29-
expectType<FindCursor<TestModel>>(collectionT.find({}));
42+
expectType<FindCursor<WithId<TestModel>>>(collectionT.find({}));
3043

3144
await collectionT.findOne(
3245
{},
@@ -72,22 +85,24 @@ interface Bag {
7285

7386
const collectionBag = db.collection<Bag>('bag');
7487

75-
const cursor: FindCursor<Bag> = collectionBag.find({ color: 'black' });
88+
const cursor: FindCursor<WithId<Bag>> = collectionBag.find({ color: 'black' });
7689

7790
cursor.toArray((_err, bags) => {
78-
expectType<Bag[] | undefined>(bags);
91+
expectType<WithId<Bag>[] | undefined>(bags);
7992
});
8093

8194
cursor.forEach(
8295
bag => {
83-
expectType<Bag>(bag);
96+
expectType<WithId<Bag>>(bag);
8497
},
8598
() => {
8699
return null;
87100
}
88101
);
89102

90-
expectType<Bag | null>(await collectionBag.findOne({ color: 'red' }, { projection: { cost: 1 } }));
103+
expectType<WithId<Bag> | null>(
104+
await collectionBag.findOne({ color: 'red' }, { projection: { cost: 1 } })
105+
);
91106

92107
const overrideFind = await collectionBag.findOne<{ cost: number }>(
93108
{ color: 'white' },
@@ -150,40 +165,48 @@ const colorsFreeze: ReadonlyArray<string> = Object.freeze(['blue', 'red']);
150165
const colorsWritable: Array<string> = ['blue', 'red'];
151166

152167
// Permitted Readonly fields
153-
expectType<FindCursor<{ color: string }>>(colorCollection.find({ color: { $in: colorsFreeze } }));
154-
expectType<FindCursor<{ color: string }>>(colorCollection.find({ color: { $in: colorsWritable } }));
155-
expectType<FindCursor<{ color: string }>>(colorCollection.find({ color: { $nin: colorsFreeze } }));
156-
expectType<FindCursor<{ color: string }>>(
168+
expectType<FindCursor<WithId<{ color: string }>>>(
169+
colorCollection.find({ color: { $in: colorsFreeze } })
170+
);
171+
expectType<FindCursor<WithId<{ color: string }>>>(
172+
colorCollection.find({ color: { $in: colorsWritable } })
173+
);
174+
expectType<FindCursor<WithId<{ color: string }>>>(
175+
colorCollection.find({ color: { $nin: colorsFreeze } })
176+
);
177+
expectType<FindCursor<WithId<{ color: string }>>>(
157178
colorCollection.find({ color: { $nin: colorsWritable } })
158179
);
159180
// $all and $elemMatch works against single fields (it's just redundant)
160-
expectType<FindCursor<{ color: string }>>(colorCollection.find({ color: { $all: colorsFreeze } }));
161-
expectType<FindCursor<{ color: string }>>(
181+
expectType<FindCursor<WithId<{ color: string }>>>(
182+
colorCollection.find({ color: { $all: colorsFreeze } })
183+
);
184+
expectType<FindCursor<WithId<{ color: string }>>>(
162185
colorCollection.find({ color: { $all: colorsWritable } })
163186
);
164-
expectType<FindCursor<{ color: string }>>(
187+
expectType<FindCursor<WithId<{ color: string }>>>(
165188
colorCollection.find({ color: { $elemMatch: colorsFreeze } })
166189
);
167-
expectType<FindCursor<{ color: string }>>(
190+
expectType<FindCursor<WithId<{ color: string }>>>(
168191
colorCollection.find({ color: { $elemMatch: colorsWritable } })
169192
);
170193

171194
const countCollection = client.db('test_db').collection<{ count: number }>('test_collection');
172-
expectType<FindCursor<{ count: number }>>(
195+
expectType<FindCursor<WithId<{ count: number }>>>(
173196
countCollection.find({ count: { $bitsAnySet: Object.freeze([1, 0, 1]) } })
174197
);
175-
expectType<FindCursor<{ count: number }>>(
198+
expectType<FindCursor<WithId<{ count: number }>>>(
176199
countCollection.find({ count: { $bitsAnySet: [1, 0, 1] as number[] } })
177200
);
178201

179202
const listsCollection = client.db('test_db').collection<{ lists: string[] }>('test_collection');
180203
await listsCollection.updateOne({}, { list: { $pullAll: Object.freeze(['one', 'two']) } });
181-
expectType<FindCursor<{ lists: string[] }>>(listsCollection.find({ lists: { $size: 1 } }));
204+
expectType<FindCursor<WithId<{ lists: string[] }>>>(listsCollection.find({ lists: { $size: 1 } }));
182205

183206
const rdOnlyListsCollection = client
184207
.db('test_db')
185208
.collection<{ lists: ReadonlyArray<string> }>('test_collection');
186-
expectType<FindCursor<{ lists: ReadonlyArray<string> }>>(
209+
expectType<FindCursor<WithId<{ lists: ReadonlyArray<string> }>>>(
187210
rdOnlyListsCollection.find({ lists: { $size: 1 } })
188211
);
189212

@@ -196,7 +219,9 @@ expectNotType<FindCursor<{ color: string | { $in: ReadonlyArray<string> } }>>(
196219
expectNotType<FindCursor<{ color: { $in: number } }>>(
197220
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
198221
);
199-
expectType<FindCursor<{ color: string }>>(colorCollection.find({ color: { $in: 3 as any } }));
222+
expectType<FindCursor<WithId<{ color: string }>>>(
223+
colorCollection.find({ color: { $in: 3 as any } })
224+
);
200225

201226
// When you use the override, $in doesn't permit readonly
202227
colorCollection.find<{ color: string }>({ color: { $in: colorsFreeze } });
@@ -228,12 +253,40 @@ interface TypedDb extends Db {
228253
const typedDb = client.db('test2') as TypedDb;
229254

230255
const person = typedDb.collection('people').findOne({});
231-
expectType<Promise<Person | null>>(person);
256+
expectType<Promise<WithId<Person> | null>>(person);
232257

233258
typedDb.collection('people').findOne({}, function (_err, person) {
234-
expectType<Person | null | undefined>(person); // null is if nothing is found, undefined is when there is an error defined
259+
expectType<WithId<Person> | null | undefined>(person); // null is if nothing is found, undefined is when there is an error defined
235260
});
236261

237262
typedDb.collection('things').findOne({}, function (_err, thing) {
238-
expectType<Thing | null | undefined>(thing);
263+
expectType<WithId<Thing> | null | undefined>(thing);
239264
});
265+
266+
interface SchemaWithTypicalId {
267+
_id: ObjectId;
268+
name: string;
269+
}
270+
const schemaWithTypicalIdCol = db.collection<SchemaWithTypicalId>('a');
271+
expectType<WithId<SchemaWithTypicalId> | null>(await schemaWithTypicalIdCol.findOne());
272+
expectAssignable<SchemaWithTypicalId | null>(await schemaWithTypicalIdCol.findOne());
273+
274+
interface SchemaWithOptionalTypicalId {
275+
_id?: ObjectId;
276+
name: string;
277+
}
278+
const schemaWithOptionalTypicalId = db.collection<SchemaWithOptionalTypicalId>('a');
279+
expectType<WithId<SchemaWithOptionalTypicalId> | null>(await schemaWithOptionalTypicalId.findOne());
280+
expectAssignable<SchemaWithOptionalTypicalId | null>(await schemaWithOptionalTypicalId.findOne());
281+
282+
interface SchemaWithUserDefinedId {
283+
_id: number;
284+
name: string;
285+
}
286+
const schemaWithUserDefinedId = db.collection<SchemaWithUserDefinedId>('a');
287+
expectType<WithId<SchemaWithUserDefinedId> | null>(await schemaWithUserDefinedId.findOne());
288+
const result = await schemaWithUserDefinedId.findOne();
289+
if (result !== null) {
290+
expectType<number>(result._id);
291+
}
292+
expectAssignable<SchemaWithUserDefinedId | null>(await schemaWithUserDefinedId.findOne());

test/types/mongodb.test-d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { AggregationCursor } from '../../src/cursor/aggregation_cursor';
55
import type { FindCursor } from '../../src/cursor/find_cursor';
66
import type { ChangeStreamDocument } from '../../src/change_stream';
77
import type { Document } from 'bson';
8-
import { Db } from '../../src';
8+
import { Db, WithId } from '../../src';
99
import { Topology } from '../../src/sdam/topology';
1010
import * as MongoDBDriver from '../../src';
1111

@@ -30,7 +30,7 @@ const client = new MongoClient('');
3030
const db = client.db('test');
3131
const coll = db.collection('test');
3232
const findCursor = coll.find();
33-
expectType<Document | null>(await findCursor.next());
33+
expectType<WithId<Document> | null>(await findCursor.next());
3434
const mappedFind = findCursor.map<number>(obj => Object.keys(obj).length);
3535
expectType<FindCursor<number>>(mappedFind);
3636
expectType<number | null>(await mappedFind.next());

test/types/union_schema.test-d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expectType, expectError, expectNotType, expectNotAssignable, expectAssi
22

33
import type { Collection } from '../../src/collection';
44
import { ObjectId } from '../../src/bson';
5-
import type { Filter } from '../../src/mongo_types';
5+
import type { Filter, WithId } from '../../src/mongo_types';
66

77
type InsertOneFirstParam<Schema> = Parameters<Collection<Schema>['insertOne']>[0];
88

@@ -31,7 +31,7 @@ expectAssignable<ShapeInsert>({ height: 4, width: 4 });
3131
expectAssignable<ShapeInsert>({ radius: 4 });
3232

3333
const c: Collection<Shape> = null as never;
34-
expectType<Promise<Shape | null>>(c.findOne({ height: 4, width: 4 }));
34+
expectType<Promise<WithId<Shape> | null>>(c.findOne({ height: 4, width: 4 }));
3535
// collection API can only respect TSchema given, cannot pick a type inside a union
3636
expectNotType<Promise<Rectangle | null>>(c.findOne({ height: 4, width: 4 }));
3737

0 commit comments

Comments
 (0)