feat(@types/mongodb): better support for Schemas with "any" indexed type (#40943)

This commit is contained in:
Hossein Saniei
2019-12-23 20:16:55 +03:30
committed by Andrew Branch
parent 55807cad7f
commit e19dc99582
3 changed files with 216 additions and 39 deletions

View File

@@ -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<T, K> = Pick<T, Exclude<keyof T, K>>;
export function connect(uri: string, options?: MongoClientOptions): Promise<MongoClient>;
@@ -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<TSchema = Default>(name: string, callback?: MongoCallback<Collection<TSchema>>): Collection<TSchema>;
collection<TSchema = Default>(name: string, options: DbCollectionOptions, callback?: MongoCallback<Collection<TSchema>>): Collection<TSchema>;
collection<TSchema = DefaultSchema>(name: string, callback?: MongoCallback<Collection<TSchema>>): Collection<TSchema>;
collection<TSchema = DefaultSchema>(name: string, options: DbCollectionOptions, callback?: MongoCallback<Collection<TSchema>>): Collection<TSchema>;
/** http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#collections */
collections(): Promise<Array<Collection<Default>>>;
collections(callback: MongoCallback<Array<Collection<Default>>>): void;
@@ -636,9 +636,9 @@ export class Db extends EventEmitter {
command(command: object, options?: { readPreference: ReadPreferenceOrMode, session?: ClientSession }): Promise<any>;
command(command: object, options: { readPreference: ReadPreferenceOrMode, session?: ClientSession }, callback: MongoCallback<any>): void;
/** http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#createCollection */
createCollection<TSchema = Default>(name: string, callback: MongoCallback<Collection<TSchema>>): void;
createCollection<TSchema = Default>(name: string, options?: CollectionCreateOptions): Promise<Collection<TSchema>>;
createCollection<TSchema = Default>(name: string, options: CollectionCreateOptions, callback: MongoCallback<Collection<TSchema>>): void;
createCollection<TSchema = DefaultSchema>(name: string, callback: MongoCallback<Collection<TSchema>>): void;
createCollection<TSchema = DefaultSchema>(name: string, options?: CollectionCreateOptions): Promise<Collection<TSchema>>;
createCollection<TSchema = DefaultSchema>(name: string, options: CollectionCreateOptions, callback: MongoCallback<Collection<TSchema>>): void;
/** http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#createIndex */
createIndex(name: string, fieldOrSpec: string | object, callback: MongoCallback<any>): void;
createIndex(name: string, fieldOrSpec: string | object, options?: IndexOptions): Promise<any>;
@@ -673,9 +673,9 @@ export class Db extends EventEmitter {
removeUser(username: string, options?: CommonOptions): Promise<any>;
removeUser(username: string, options: CommonOptions, callback: MongoCallback<any>): void;
/** http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#renameCollection */
renameCollection<TSchema = Default>(fromCollection: string, toCollection: string, callback: MongoCallback<Collection<TSchema>>): void;
renameCollection<TSchema = Default>(fromCollection: string, toCollection: string, options?: { dropTarget?: boolean }): Promise<Collection<TSchema>>;
renameCollection<TSchema = Default>(fromCollection: string, toCollection: string, options: { dropTarget?: boolean }, callback: MongoCallback<Collection<TSchema>>): void;
renameCollection<TSchema = DefaultSchema>(fromCollection: string, toCollection: string, callback: MongoCallback<Collection<TSchema>>): void;
renameCollection<TSchema = DefaultSchema>(fromCollection: string, toCollection: string, options?: { dropTarget?: boolean }): Promise<Collection<TSchema>>;
renameCollection<TSchema = DefaultSchema>(fromCollection: string, toCollection: string, options: { dropTarget?: boolean }, callback: MongoCallback<Collection<TSchema>>): void;
/** http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#setProfilingLevel */
setProfilingLevel(level: ProfilingLevel, callback: MongoCallback<ProfilingLevel>): void;
setProfilingLevel(level: ProfilingLevel, options?: { session?: ClientSession }): Promise<ProfilingLevel>;
@@ -867,7 +867,10 @@ export interface FSyncOptions extends CommonOptions {
fsync?: boolean;
}
type OptionalId<TSchema> = Omit<TSchema, '_id'> & { _id?: any };
// TypeScript Omit (Exclude to be specific) does not work for objects with an "any" indexed type
type EnhancedOmit<T, K> =
string | number extends keyof T ? T : // T has indexed type e.g. { _id: string; [k: string]: any; } or it is "any"
Omit<T, K>;
type ExtractIdType<TSchema> =
TSchema extends { _id: infer U } // user has defined a type for _id
@@ -875,12 +878,19 @@ type ExtractIdType<TSchema> =
unknown extends U ? ObjectId : U
: ObjectId; // user has not defined _id on schema
// this makes _id optional
type OptionalId<TSchema extends { _id?: any }> =
ObjectId extends TSchema['_id']
// a Schema with ObjectId _id type or "any" or "indexed type" provided
? EnhancedOmit<TSchema, '_id'> & { _id?: ExtractIdType<TSchema> }
// a Schema provided but _id type is not ObjectId
: WithId<TSchema>;
// this adds _id as a required property
type WithId<TSchema> =
Omit<TSchema, '_id'> & { _id: ExtractIdType<TSchema> };
type WithId<TSchema> = EnhancedOmit<TSchema, '_id'> & { _id: ExtractIdType<TSchema> };
/** http://mongodb.github.io/node-mongodb-native/3.1/api/Collection.html */
export interface Collection<TSchema = Default> {
export interface Collection<TSchema extends { [key: string]: any } = DefaultSchema> {
/**
* 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<TSchema = Default> {
export interface FindAndModifyWriteOpResultObject<TSchema> {
//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<TSchema extends Record<string, any>> {
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<TSchema extends { _id: any }> {
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<TSchema extends Record<string, any>> {
export interface InsertOneWriteOpResult<TSchema extends { _id: any }> {
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<T = Default> extends Readable {

View File

@@ -31,16 +31,16 @@ async function run() {
}
const cursor: Cursor<Bag> = collection.find<Bag>({ 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<Bag>({ color: 'white' }).then(b => {
b.cost;

View File

@@ -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<TestModel>('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<TestModelWithCustomId>('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<TestModelWithCustomObjectId>('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<IndexTypeTestModel>('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<IndexTypeTestModelWithId>('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<IndexTypeTestModelWithObjectId>('testCollection');
// TODO: should not demand _id if it is ObjectId
// await indexTypeCollection3.insertOne({ stringField: 'hola', numberField: 23, randomField: [34, 54, 32], randomFiel2: 32 });
}