diff --git a/types/mongodb/index.d.ts b/types/mongodb/index.d.ts index a15ccfca85..dbfbef656d 100644 --- a/types/mongodb/index.d.ts +++ b/types/mongodb/index.d.ts @@ -39,7 +39,7 @@ import { EventEmitter } from 'events'; import { Readable, Writable } from 'stream'; import { checkServerIdentity } from 'tls'; -// This line can be removed after minimum required TypeScript Version is above 3.5 +// We can use TypeScript Omit once minimum required TypeScript Version is above 3.5 type Omit = Pick>; export function connect(uri: string, options?: MongoClientOptions): Promise; @@ -626,8 +626,8 @@ export class Db extends EventEmitter { /** http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#admin */ admin(): Admin; /** http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#collection */ - collection(name: string, callback?: MongoCallback>): Collection; - collection(name: string, options: DbCollectionOptions, callback?: MongoCallback>): Collection; + collection(name: string, callback?: MongoCallback>): Collection; + collection(name: string, options: DbCollectionOptions, callback?: MongoCallback>): Collection; /** http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#collections */ collections(): Promise>>; collections(callback: MongoCallback>>): void; @@ -636,9 +636,9 @@ export class Db extends EventEmitter { command(command: object, options?: { readPreference: ReadPreferenceOrMode, session?: ClientSession }): Promise; command(command: object, options: { readPreference: ReadPreferenceOrMode, session?: ClientSession }, callback: MongoCallback): void; /** http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#createCollection */ - createCollection(name: string, callback: MongoCallback>): void; - createCollection(name: string, options?: CollectionCreateOptions): Promise>; - createCollection(name: string, options: CollectionCreateOptions, callback: MongoCallback>): void; + createCollection(name: string, callback: MongoCallback>): void; + createCollection(name: string, options?: CollectionCreateOptions): Promise>; + createCollection(name: string, options: CollectionCreateOptions, callback: MongoCallback>): void; /** http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#createIndex */ createIndex(name: string, fieldOrSpec: string | object, callback: MongoCallback): void; createIndex(name: string, fieldOrSpec: string | object, options?: IndexOptions): Promise; @@ -673,9 +673,9 @@ export class Db extends EventEmitter { removeUser(username: string, options?: CommonOptions): Promise; removeUser(username: string, options: CommonOptions, callback: MongoCallback): void; /** http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#renameCollection */ - renameCollection(fromCollection: string, toCollection: string, callback: MongoCallback>): void; - renameCollection(fromCollection: string, toCollection: string, options?: { dropTarget?: boolean }): Promise>; - renameCollection(fromCollection: string, toCollection: string, options: { dropTarget?: boolean }, callback: MongoCallback>): void; + renameCollection(fromCollection: string, toCollection: string, callback: MongoCallback>): void; + renameCollection(fromCollection: string, toCollection: string, options?: { dropTarget?: boolean }): Promise>; + renameCollection(fromCollection: string, toCollection: string, options: { dropTarget?: boolean }, callback: MongoCallback>): void; /** http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#setProfilingLevel */ setProfilingLevel(level: ProfilingLevel, callback: MongoCallback): void; setProfilingLevel(level: ProfilingLevel, options?: { session?: ClientSession }): Promise; @@ -867,7 +867,10 @@ export interface FSyncOptions extends CommonOptions { fsync?: boolean; } -type OptionalId = Omit & { _id?: any }; +// TypeScript Omit (Exclude to be specific) does not work for objects with an "any" indexed type +type EnhancedOmit = + string | number extends keyof T ? T : // T has indexed type e.g. { _id: string; [k: string]: any; } or it is "any" + Omit; type ExtractIdType = TSchema extends { _id: infer U } // user has defined a type for _id @@ -875,12 +878,19 @@ type ExtractIdType = unknown extends U ? ObjectId : U : ObjectId; // user has not defined _id on schema +// this makes _id optional +type OptionalId = + ObjectId extends TSchema['_id'] + // a Schema with ObjectId _id type or "any" or "indexed type" provided + ? EnhancedOmit & { _id?: ExtractIdType } + // a Schema provided but _id type is not ObjectId + : WithId; + // this adds _id as a required property -type WithId = - Omit & { _id: ExtractIdType }; +type WithId = EnhancedOmit & { _id: ExtractIdType }; /** http://mongodb.github.io/node-mongodb-native/3.1/api/Collection.html */ -export interface Collection { +export interface Collection { /** * Get the collection name. */ @@ -1709,7 +1719,7 @@ export interface DeleteWriteOpResultObject { } /** http://mongodb.github.io/node-mongodb-native/3.1/api/Collection.html#~findAndModifyWriteOpResult */ -export interface FindAndModifyWriteOpResultObject { +export interface FindAndModifyWriteOpResultObject { //Document returned from findAndModify command. value?: TSchema; //The raw lastErrorObject returned from the command. @@ -1884,15 +1894,6 @@ export interface FindOneOptions { session?: ClientSession; } -/** http://mongodb.github.io/node-mongodb-native/3.1/api/Collection.html#~insertWriteOpResult */ -export interface InsertWriteOpResult> { - insertedCount: number; - ops: TSchema[]; - insertedIds: { [key: number]: TSchema['_id'] }; - connection: any; - result: { ok: number; n: number }; -} - /** http://mongodb.github.io/node-mongodb-native/3.1/api/Collection.html#insertOne */ export interface CollectionInsertOneOptions extends CommonOptions { /** @@ -1905,8 +1906,17 @@ export interface CollectionInsertOneOptions extends CommonOptions { bypassDocumentValidation?: boolean; } +/** http://mongodb.github.io/node-mongodb-native/3.1/api/Collection.html#~insertWriteOpResult */ +export interface InsertWriteOpResult { + insertedCount: number; + ops: TSchema[]; + insertedIds: { [key: number]: TSchema['_id'] }; + connection: any; + result: { ok: number; n: number }; +} + /** http://mongodb.github.io/node-mongodb-native/3.1/api/Collection.html#~insertOneWriteOpResult */ -export interface InsertOneWriteOpResult> { +export interface InsertOneWriteOpResult { insertedCount: number; ops: TSchema[]; insertedId: TSchema['_id']; @@ -1986,6 +1996,7 @@ export interface WriteOpResult { export type CursorResult = object | null | boolean; type Default = any; +type DefaultSchema = any; /** http://mongodb.github.io/node-mongodb-native/3.1/api/Cursor.html */ export class Cursor extends Readable { diff --git a/types/mongodb/test/collection/findX.ts b/types/mongodb/test/collection/findX.ts index a9ab9d4906..3c04858461 100644 --- a/types/mongodb/test/collection/findX.ts +++ b/types/mongodb/test/collection/findX.ts @@ -31,16 +31,16 @@ async function run() { } const cursor: Cursor = collection.find({ color: 'black' }); cursor.toArray((err, r) => { - r[0].cost; + r[0].cost; // $ExpectType number }); cursor.forEach( bag => { - bag.color; + bag.color; // $ExpectType string }, () => {}, ); collection.findOne({ color: 'white' }).then(b => { - const _b: Bag = b; + const _b: Bag = b; // b is larger than bag and may contain extra properties }); collection.findOne({ color: 'white' }).then(b => { b.cost; diff --git a/types/mongodb/test/collection/insertX.ts b/types/mongodb/test/collection/insertX.ts index bb4f6e8f21..5fd92c81cf 100644 --- a/types/mongodb/test/collection/insertX.ts +++ b/types/mongodb/test/collection/insertX.ts @@ -7,15 +7,42 @@ async function run() { const client = await connect(connectionString); const db = client.db('test'); - // test insertOne results - db.collection('test-insert').insertOne({ a: 2 }, (err, result) => { - result.insertedCount; // $ExpectType number - result.insertedId; // $ExpectType ObjectId - result.result.n; // $ExpectType number - result.result.ok; // $ExpectType number - }); + const anyCollection = db.collection('test-any-type'); - // test with collection type + // should accept any _id type when it is not provided in Schema + + /** + * test no collection type ("any") + */ + // test insertOne results + anyCollection.insertOne({ a: 2 }, (err, result) => { + result.insertedCount; // $ExpectType number + result.insertedId; // $ExpectType any + result.ops[0].a; // $ExpectType any + result.result.n; // $ExpectType number + result.result.ok; // $ExpectType number + }); + // test insertMany results + anyCollection.insertMany([{ a: 2 }], (err, result) => { + result.insertedCount; // $ExpectType number + result.insertedIds; // $ExpectType { [key: number]: any; } + result.ops[0].a; // $ExpectType any + result.result.n; // $ExpectType number + result.result.ok; // $ExpectType number + }); + // should accept _id with ObjectId type + const insertManyWithIdResult = await anyCollection.insertMany([{ _id: new ObjectId(), a: 2 }]); + insertManyWithIdResult.insertedCount; // $ExpectType number + insertManyWithIdResult.insertedIds; // $ExpectType { [key: number]: any; } + insertManyWithIdResult.ops[0].a; // $ExpectType any + insertManyWithIdResult.result.n; // $ExpectType number + insertManyWithIdResult.result.ok; // $ExpectType number + // should accept any _id type when it is not provided in Schema + await anyCollection.insertMany([{ _id: 12, a: 2 }]); + + /** + * test with collection type + */ interface TestModel { stringField: string; numberField?: number; @@ -23,6 +50,7 @@ async function run() { } type TestModelWithId = TestModel & { _id: ObjectId; }; const collection = db.collection('testCollection'); + const result = await collection.insert({ stringField: 'hola', fruitTags: ['Strawberry'], @@ -36,6 +64,11 @@ async function run() { { stringField: 'hola', numberField: 1, fruitTags: [] }, ]); + // $ExpectError + await collection.insert({ stringField: 3, fruitTags: ['Strawberry'], }); + // $ExpectError + await collection.insert({ fruitTags: ['Strawberry'], }); + // test results type // should add a _id field with ObjectId type if it does not exist on collection type result.ops[0]._id; // $ExpectType ObjectId @@ -45,21 +78,154 @@ async function run() { resultMany.insertedIds; // $ExpectType { [key: number]: ObjectId; } resultOne.insertedId; // $ExpectType ObjectId - // should add a _id field with user specified type - type TestModelWithCustomId = TestModel & { _id: number; }; + /** + * test custom _id type + */ + interface TestModelWithCustomId { + _id: number; + stringField: string; + numberField?: number; + fruitTags: string[]; + } const collectionWithId = db.collection('testCollection'); const resultOneWithId = await collectionWithId.insertOne({ + _id: 1, stringField: 'hola', fruitTags: ['Strawberry'], }); const resultManyWithId = await collectionWithId.insertMany([ - { stringField: 'hola', fruitTags: ['Apple', 'Lemon'] }, - { stringField: 'hola', numberField: 1, fruitTags: [] }, + { _id: 2, stringField: 'hola', fruitTags: ['Apple', 'Lemon'] }, + { _id: 2, stringField: 'hola', numberField: 1, fruitTags: [] }, ]); + // should demand _id if it is not ObjectId + // $ExpectError + await collectionWithId.insertOne({ stringField: 'hola', fruitTags: ['Strawberry'] }); + // $ExpectError + await collectionWithId.insertMany([ { stringField: 'hola', fruitTags: ['Apple', 'Lemon'] }, { _id: 2, stringField: 'hola', numberField: 1, fruitTags: [] } ]); + + // should not accept wrong _id type + // $ExpectError + await collectionWithId.insertMany([ { _id: new ObjectId, stringField: 'hola', fruitTags: ['Apple', 'Lemon'] }, { _id: 2, stringField: 'hola', numberField: 1, fruitTags: [] } ]); + resultOneWithId.ops[0]._id; // $ExpectType number resultOneWithId.insertedId; // $ExpectType number resultManyWithId.ops[0]._id; // $ExpectType number resultManyWithId.insertedIds; // $ExpectType { [key: number]: number; } + + /** + * test custom _id type (ObjectId) + */ + interface TestModelWithCustomObjectId { + _id: ObjectId; + stringField: string; + numberField?: number; + fruitTags: string[]; + } + const collectionWithObjectId = db.collection('testCollection'); + + // should accept ObjectId + await collectionWithObjectId.insertOne({ _id: new ObjectId, stringField: 'hola', numberField: 23, fruitTags: ['hi'] }); + // should not demand _id if it is ObjectId + await collectionWithObjectId.insertOne({ stringField: 'hola', numberField: 23, fruitTags: ['hi'] }); + + /** + * test indexed types + */ + interface IndexTypeTestModel { + stringField: string; + numberField?: number; + [key: string]: any; + } + const indexTypeCollection1 = db.collection('testCollection'); + + const indexTypeResult1 = await indexTypeCollection1.insertOne({ + stringField: 'hola', + numberField: 23, + randomField: [34, 54, 32], + randomFiel2: 32, + }); + const indexTypeResultMany1 = await indexTypeCollection1.insertMany([ + { stringField: 'hola', numberField: 0 }, + { _id: new ObjectId, stringField: 'hola', randomField: [34, 54, 32] }, + ]); + + // should not accept wrong _id type + // $ExpectError + await indexTypeCollection1.insertMany([ { _id: 12, stringField: 'hola', numberField: 0 } ]); + // should not accept wrong types for fields + // $ExpectError + await indexTypeCollection1.insert({ stringField: 3, randomField: [34, 54, 32] }); + // should demand missing fields + // $ExpectError + await indexTypeCollection1.insertMany([ { randomField: [34, 54, 32] } ]); + + indexTypeResult1.ops[0]._id; // $ExpectType ObjectId + indexTypeResult1.insertedId; // $ExpectType ObjectId + // should not remove types of existing fields + indexTypeResult1.ops[0].stringField; // $ExpectType string + indexTypeResult1.ops[0].numberField; // $ExpectType number + // should assign "any" type to any other field + indexTypeResult1.ops[0].randomField; // $ExpectType any + // should do the same for insertMany + indexTypeResultMany1.ops[0]._id; // $ExpectType ObjectId + indexTypeResultMany1.insertedIds; // $ExpectType { [key: number]: ObjectId; } + indexTypeResultMany1.ops[0].numberField; // $ExpectType number + indexTypeResultMany1.ops[0].stringField; // $ExpectType string + indexTypeResultMany1.ops[0].randomField; // $ExpectType any + + /** + * test indexed types with custom _id (not ObjectId) + */ + interface IndexTypeTestModelWithId { + _id: number; + stringField: string; + numberField?: number; + [key: string]: any; + } + const indexTypeCollection2 = db.collection('testCollection'); + + const indexTypeResult2 = await indexTypeCollection2.insertOne({ + _id: 1, + stringField: 'hola', + numberField: 23, + randomField: [34, 54, 32], + randomFiel2: 32, + }); + const indexTypeResultMany2 = await indexTypeCollection2.insertMany([ + { _id: 1, stringField: 'hola', numberField: 0 }, + { _id: 2, stringField: 'hola', randomField: [34, 54, 32] }, + ]); + + // should only accept _id type provided in Schema + // $ExpectError + await indexTypeCollection2.insertOne({ _id: '12', stringField: 'hola', numberField: 23, randomField: [34, 54, 32], randomFiel2: 32 }); + // $ExpectError + await indexTypeCollection2.insertMany([ { _id: '1', stringField: 'hola', numberField: 0 }, { _id: 2, stringField: 'hola', randomField: [34, 54, 32] } ]); + + // should demand _id if it is defined and is not ObjectId + // $ExpectError + await indexTypeCollection2.insertOne({ stringField: 'hola', numberField: 23, randomField: [34, 54, 32], randomFiel2: 32 }); + // $ExpectError + await indexTypeCollection2.insertMany([ { stringField: 'hola', numberField: 0 }, { _id: 12, stringField: 'hola', randomField: [34, 54, 32] } ]); + + indexTypeResult2.ops[0]._id; // $ExpectType number + indexTypeResult2.insertedId; // $ExpectType number + indexTypeResultMany2.ops[0]._id; // $ExpectType number + indexTypeResultMany2.insertedIds; // $ExpectType { [key: number]: number; } + + /** + * test indexed types with custom _id (ObjectId) + */ + interface IndexTypeTestModelWithObjectId { + _id: ObjectId; + stringField: string; + numberField?: number; + [key: string]: any; + } + const indexTypeCollection3 = db.collection('testCollection'); + + // TODO: should not demand _id if it is ObjectId + // await indexTypeCollection3.insertOne({ stringField: 'hola', numberField: 23, randomField: [34, 54, 32], randomFiel2: 32 }); }