mirror of
https://github.com/gosticks/DefinitelyTyped.git
synced 2026-06-28 22:30:01 +00:00
feat(@types/mongodb): better support for Schemas with "any" indexed type (#40943)
This commit is contained in:
committed by
Andrew Branch
parent
55807cad7f
commit
e19dc99582
59
types/mongodb/index.d.ts
vendored
59
types/mongodb/index.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user