diff --git a/docs/guide.md b/docs/guide.md index d78226415f5..ed64cc5a521 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -143,9 +143,18 @@ We may also define our own custom document instance methods. ```javascript // define a schema -const animalSchema = new Schema({ name: String, type: String }); +const animalSchema = new Schema({ name: String, type: String }, +{ + // Assign a function to the "methods" object of our animalSchema through schema options. + // By following this approach, there is no need to create a separate TS type to define the type of the instance functions. + methods:{ + findSimilarTypes(cb){ + return mongoose.model('Animal').find({ type: this.type }, cb); + } + } +}); -// assign a function to the "methods" object of our animalSchema +// Or, assign a function to the "methods" object of our animalSchema animalSchema.methods.findSimilarTypes = function(cb) { return mongoose.model('Animal').find({ type: this.type }, cb); }; @@ -169,14 +178,28 @@ dog.findSimilarTypes((err, dogs) => {

Statics

-You can also add static functions to your model. There are two equivalent +You can also add static functions to your model. There are three equivalent ways to add a static: +- Add a function property to the second argument of the schema-constructor (`statics`) - Add a function property to `schema.statics` - Call the [`Schema#static()` function](/docs/api.html#schema_Schema-static) ```javascript -// Assign a function to the "statics" object of our animalSchema + +// define a schema +const animalSchema = new Schema({ name: String, type: String }, +{ + // Assign a function to the "statics" object of our animalSchema through schema options. + // By following this approach, there is no need to create a separate TS type to define the type of the statics functions. + statics:{ + findByName(name){ + return this.find({ name: new RegExp(name, 'i') }); + } + } +}); + +// Or, Assign a function to the "statics" object of our animalSchema animalSchema.statics.findByName = function(name) { return this.find({ name: new RegExp(name, 'i') }); }; @@ -197,6 +220,20 @@ but for mongoose queries. Query helper methods let you extend mongoose's [chainable query builder API](./queries.html). ```javascript + +// define a schema +const animalSchema = new Schema({ name: String, type: String }, +{ + // Assign a function to the "query" object of our animalSchema through schema options. + // By following this approach, there is no need to create a separate TS type to define the type of the query functions. + query:{ + byName(name){ + return this.where({ name: new RegExp(name, 'i') }) + } + } +}); + +// Or, Assign a function to the "query" object of our animalSchema animalSchema.query.byName = function(name) { return this.where({ name: new RegExp(name, 'i') }) }; @@ -409,6 +446,7 @@ Valid options: - [read](#read) - [writeConcern](#writeConcern) - [shardKey](#shardKey) +- [statics](#statics) - [strict](#strict) - [strictQuery](#strictQuery) - [toJSON](#toJSON) @@ -423,6 +461,8 @@ Valid options: - [skipVersioning](#skipVersioning) - [timestamps](#timestamps) - [storeSubdocValidationError](#storeSubdocValidationError) +- [methods](#methods) +- [query](#query-helpers)

option: autoIndex

diff --git a/docs/typescript/query-helpers.md b/docs/typescript/query-helpers.md index a9f99f6b32f..7c108d465f2 100644 --- a/docs/typescript/query-helpers.md +++ b/docs/typescript/query-helpers.md @@ -14,7 +14,10 @@ var Project = mongoose.model('Project', ProjectSchema); Project.find().where('stars').gt(1000).byName('mongoose'); ``` -In TypeScript, Mongoose's `Model` takes 3 generic parameters: +In TypeScript, Mongoose does support manually typed and automatically typed Query Helpers. + +1- Manually typed: +Mongoose's `Model` takes 3 generic parameters: 1. The `DocType` 2. a `TQueryHelpers` type @@ -58,4 +61,32 @@ async function run(): Promise { // Equivalent to `ProjectModel.find({ stars: { $gt: 1000 }, name: 'mongoose' })` await ProjectModel.find().where('stars').gt(1000).byName('mongoose'); } +``` + +2- Automatically typed: +Mongoose does support auto typed Query Helpers that it are supplied in schema options. +Query Helpers functions can be defined as following: + +```typescript +import { Schema, model } from 'mongoose'; + + const schema = new Schema({ + name: { type: String, required: true }, + stars: { type: Number, required: true } + }, { + query: { + byName(name) { + return this.find({ name: name }); + } + } + }); + + const ProjectModel = model( + 'Project', + schema + ); + + // Equivalent to `ProjectModel.find({ stars: { $gt: 1000 }, name: 'mongoose' })` + await ProjectModel.find().where('stars').gt(1000).byName('mongoose'); +} ``` \ No newline at end of file diff --git a/docs/typescript/schemas.md b/docs/typescript/schemas.md index 79299b49834..001b3ab0f03 100644 --- a/docs/typescript/schemas.md +++ b/docs/typescript/schemas.md @@ -1,7 +1,11 @@ # Schemas in TypeScript Mongoose [schemas](/docs/guide.html) are how you tell Mongoose what your documents look like. -Mongoose schemas are separate from TypeScript interfaces, so you need to define both a _document interface_ and a _schema_. +Mongoose schemas are separate from TypeScript interfaces, so you need to define both a _document interface_ and a _schema_ until V6.3.1. +Mongoose supports auto typed schemas so you don't need to define additional typescript interface anymore but you are still able to do so. +Mongoose provides a `InferSchemaType`, which infers the type of the auto typed schema document when needed. + +`Until mongoose V6.3.1:` ```typescript import { Schema } from 'mongoose'; @@ -21,6 +25,37 @@ const schema = new Schema({ }); ``` +`another approach:` + +```typescript +import { Schema, InferSchemaType } from 'mongoose'; + +// Document interface +// No need to define TS interface any more. +// interface User { +// name: string; +// email: string; +// avatar?: string; +// } + +// Schema +const schema = new Schema({ + name: { type: String, required: true }, + email: { type: String, required: true }, + avatar: String +}); + +type User = InferSchemaType; +// InferSchemaType will determine the type as follows: +// type User = { +// name: string; +// email: string; +// avatar?: string; +// } + + +``` + By default, Mongoose does **not** check if your document interface lines up with your schema. For example, the above code won't throw an error if `email` is optional in the document interface, but `required` in `schema`. @@ -51,7 +86,7 @@ Mongoose wraps `DocType` in a Mongoose document for cases like the `this` parame For example: ```typescript -schema.pre('save', function(): void { +schema.pre('save', function (): void { console.log(this.name); // TypeScript knows that `this` is a `mongoose.Document & User` by default }); ``` @@ -102,7 +137,7 @@ interface User { const schema = new Schema>({ name: { type: String, required: true }, email: { type: String, required: true }, - avatar: String + avatar: String, }); ``` @@ -121,13 +156,13 @@ interface BlogPost { } interface User { - tags: Types.Array, - blogPosts: Types.DocumentArray + tags: Types.Array; + blogPosts: Types.DocumentArray; } const schema = new Schema>({ tags: [String], - blogPosts: [{ title: String }] + blogPosts: [{ title: String }], }); ``` @@ -139,4 +174,4 @@ If you use `Types.DocumentArray` in the above case, you'll be able to `push()` a const user = new User({ blogPosts: [] }); user.blogPosts.push({ title: 'test' }); // Would not work if you did `blogPosts: BlogPost[]` -``` \ No newline at end of file +``` diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 6897ccad225..b377538f006 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -65,6 +65,27 @@ const User = model('User', schema); const answer: number = User.myStaticMethod(); // 42 ``` +Mongoose does support auto typed static functions now that it is supplied in schema options. +Statics functions can be defined as following: + +```typescript +import { Schema, model } from 'mongoose'; + +const schema = new Schema( + { name: String }, + { + statics: { + myStaticMethod() { + return 42; + }, + }, + } +); + +const User = model('User', schema); + +const answer = User.myStaticMethod(); // 42 +``` ## Both Methods and Statics Below is how you can define a model that has both methods and statics. diff --git a/docs/typescript/statics.md b/docs/typescript/statics.md new file mode 100644 index 00000000000..c8f163c8b37 --- /dev/null +++ b/docs/typescript/statics.md @@ -0,0 +1,47 @@ +# Statics in TypeScript + +Mongoose [models](/docs/models.html) do **not** have an explicit generic parameter for [statics](/docs/guide.html#statics). +If your model has statics, we recommend creating an interface that [extends](https://www.typescriptlang.org/docs/handbook/interfaces.html) Mongoose's `Model` interface as shown below. + +```typescript +import { Model, Schema, model } from 'mongoose'; + +interface IUser { + name: string; +} + +interface UserModel extends Model { + myStaticMethod(): number; +} + +const schema = new Schema({ name: String }); +schema.static('myStaticMethod', function myStaticMethod() { + return 42; +}); + +const User = model('User', schema); + +const answer: number = User.myStaticMethod(); // 42 +``` + +Mongoose does support auto typed static functions that it are supplied in schema options. +Static functions can be defined by: + +```typescript +import { Schema, model } from 'mongoose'; + +const schema = new Schema( + { name: String }, + { + statics: { + myStaticMethod() { + return 42; + }, + }, + } +); + +const User = model('User', schema); + +const answer = User.myStaticMethod(); // 42 +``` diff --git a/lib/schema.js b/lib/schema.js index 9de0f395e54..3ea4c3b5e32 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -107,11 +107,11 @@ function Schema(obj, options) { this.inherits = {}; this.callQueue = []; this._indexes = []; - this.methods = {}; + this.methods = (options && options.methods) || {}; this.methodOptions = {}; - this.statics = {}; + this.statics = (options && options.statics) || {}; this.tree = {}; - this.query = {}; + this.query = (options && options.query) || {}; this.childSchemas = []; this.plugins = []; // For internal debugging. Do not use this to try to save a schema in MDB. diff --git a/test/document.test.js b/test/document.test.js index 115d87a10d8..e1202951975 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -11455,3 +11455,12 @@ describe('document', function() { assert.ok(baz.populated('bar')); }); }); + +describe('Check if instance function that is supplied in schema option is availabe', function() { + it('should give an instance function back rather than undefined', function ModelJS() { + const testSchema = new mongoose.Schema({}, { methods: { instanceFn() { return 'Returned from DocumentInstanceFn'; } } }); + const TestModel = mongoose.model('TestModel', testSchema); + const TestDocument = new TestModel({}); + assert.equal(TestDocument.instanceFn(), 'Returned from DocumentInstanceFn'); + }); +}); \ No newline at end of file diff --git a/test/model.test.js b/test/model.test.js index 52a864058df..cf2f53c60ed 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -8505,6 +8505,13 @@ describe('Model', function() { }); }); +describe('Check if static function that is supplied in schema option is available', function() { + it('should give a static function back rather than undefined', function ModelJS() { + const testSchema = new mongoose.Schema({}, { statics: { staticFn() { return 'Returned from staticFn'; } } }); + const TestModel = mongoose.model('TestModel', testSchema); + assert.equal(TestModel.staticFn(), 'Returned from staticFn'); + }); +}); diff --git a/test/query.test.js b/test/query.test.js index d09a4135bdc..2f621c6cd38 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -3962,6 +3962,36 @@ describe('Query', function() { assert.equal(result.length, 1); assert.equal(result[0].name, '@foo.com'); }); + + it('should return query helper supplied in schema options query property instead of undefined', function(done) { + const Model = db.model('Test', new Schema({ + userName: { + type: String, + required: [true, 'userName is required'] + } + }, { + query: { + byUserName(userName) { + return this.where({ userName }); + } + } + })); + + Model.create({ userName: 'test' }, function(error) { + if (error instanceof Error) { + return done(error); + } + Model.find().byUserName('test').exec(function(error, docs) { + if (error instanceof Error) { + return done(error); + } + assert.equal(docs.length, 1); + assert.equal(docs[0].userName, 'test'); + done(); + }); + }); + }); + it('allows a transform option for lean on a query gh-10423', async function() { const arraySchema = new mongoose.Schema({ sub: String diff --git a/test/types/create.test.ts b/test/types/create.test.ts index 4492d113fca..debca167aeb 100644 --- a/test/types/create.test.ts +++ b/test/types/create.test.ts @@ -1,23 +1,185 @@ -import { Schema, model, Document, Types } from 'mongoose'; +import { Schema, model, Types, CallbackError } from 'mongoose'; +import { expectError, expectType } from 'tsd'; const schema: Schema = new Schema({ name: { type: 'String' } }); -interface ITest extends Document { +interface ITest { _id?: Types.ObjectId; name?: string; } const Test = model('Test', schema); -Test.create({ _id: '0'.repeat(24), name: 'test' }).then((doc: ITest) => console.log(doc.name)); +Test.create({ _id: '000000000000000000000000', name: 'test' }).then(doc => { + expectType(doc._id); + expectType(doc.name); + expectType(doc.isNew); +}); -Test.create([{ name: 'test' }], { validateBeforeSave: false }).then((docs: ITest[]) => console.log(docs[0].name)); +Test.create({ _id: new Types.ObjectId('000000000000000000000000'), name: 'test' }).then((doc) => { + expectType(doc._id); + expectType(doc.name); + expectType(doc.isNew); +}); -Test.create({ name: 'test' }, { name: 'test2' }).then((docs: ITest[]) => console.log(docs[0].name)); +Test.create([{ name: 'test' }], { validateBeforeSave: false }).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); -Test.insertMany({ name: 'test' }).then((docs: ITest[]) => console.log(docs[0].name)); +Test.create({ name: 'test' }, { name: 'test2' }).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[1]._id); + expectType(docs[1].name); +}); -Test.create([{ name: 'test' }], { validateBeforeSave: true }).then((docs: ITest[]) => console.log(docs[0].name)); +Test.create([{ name: 'test' }], { validateBeforeSave: true }).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); +}); + + +Test.insertMany({ name: 'test' }, {}, (err, docs) => { + expectType(err); + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany({ name: 'test' }, { lean: true }, (err, docs) => { + expectType(err); + expectType(docs[0]._id); + expectType(docs[0].name); + expectError(docs[0].isNew); +}); + +Test.insertMany({ name: 'test' }, (err, docs) => { + expectType(err); + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany({ name: 'test' }, {}, (err, docs) => { + expectType(err); + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany([{ name: 'test' }], { rawResult: true }, (err, result) => { + expectType(err); + expectType(result.acknowledged); + expectType(result.insertedCount); + expectType<{ [key: number]: Types.ObjectId; }>(result.insertedIds); +}); + +Test.insertMany([{ name: 'test' }], { rawResult: true }, (err, result) => { + expectType(err); + expectType(result.acknowledged); + expectType(result.insertedCount); + expectType<{ [key: number]: Types.ObjectId; }>(result.insertedIds); +}); + +Test.insertMany([{ name: 'test' }], { lean: true }, (err, docs) => { + expectType(err); + expectType(docs[0]._id); + expectType(docs[0].name); + expectError(docs[0].isNew); +}); + +Test.insertMany([{ name: 'test' }], (err, docs) => { + expectType(err); + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany({ _id: '000000000000000000000000', name: 'test' }, (err, docs) => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany({ _id: new Types.ObjectId('000000000000000000000000') }, (err, docs) => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany({ name: 'test' }, {}).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany({ name: 'test' }, { lean: true }).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectError(docs[0].isNew); +}); + +Test.insertMany({ name: 'test' }).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany({ name: 'test' }, {}).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany([{ name: 'test' }], { rawResult: true }).then(result => { + expectType(result.acknowledged); + expectType(result.insertedCount); + expectType<{ [key: number]: Types.ObjectId; }>(result.insertedIds); +}); + +Test.insertMany([{ name: 'test' }], { rawResult: true }).then(result => { + expectType(result.acknowledged); + expectType(result.insertedCount); + expectType<{ [key: number]: Types.ObjectId; }>(result.insertedIds); +}); + +Test.insertMany([{ name: 'test' }], { lean: true }).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectError(docs[0].isNew); +}); + +Test.insertMany([{ name: 'test' }], { lean: false }).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany([{ name: 'test' }], { }).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany([{ name: 'test' }]).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany({ _id: '000000000000000000000000', name: 'test' }).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); + +Test.insertMany({ _id: new Types.ObjectId('000000000000000000000000'), name: 'test' }).then(docs => { + expectType(docs[0]._id); + expectType(docs[0].name); + expectType(docs[0].isNew); +}); (async() => { const [t1] = await Test.create([{ name: 'test' }]); diff --git a/test/types/document.test.ts b/test/types/document.test.ts index 3f8d8ce2176..3319a4a59a6 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -1,5 +1,7 @@ import { Schema, model, Model, Document, Types } from 'mongoose'; -import { expectError, expectType } from 'tsd'; +import { expectAssignable, expectError, expectType } from 'tsd'; +import { autoTypedModel } from './models.test'; +import { AutoTypedSchemaType } from './schema.test'; const Drink = model('Drink', new Schema({ name: String @@ -52,15 +54,16 @@ void async function run() { test.validateSync({ pathsToSkip: ['name', 'age'] }); test.validateSync({ pathsToSkip: 'name age' }); test.validateSync({ pathsToSkip: 'name age', blub: 1 }); - expectType>(test.save()); - expectType>(test.save({})); + const x = test.save(); + expectAssignable>(test.save()); + expectAssignable>(test.save({})); expectType(test.save({}, (err, doc) => { expectType(err); - expectType(doc); + expectAssignable(doc); })); expectType(test.save((err, doc) => { expectType(err); - expectType(doc); + expectAssignable(doc); })); })(); @@ -180,4 +183,19 @@ function gh11435() { async function gh11598() { const doc = await Test.findOne().orFail(); doc.populate('favoritDrink', undefined, model('temp', new Schema())); +} + +function autoTypedDocument() { + const AutoTypedModel = autoTypedModel(); + const AutoTypeModelInstance = new AutoTypedModel({ unExistProperty: 1, description: 2 }); + + expectType(AutoTypeModelInstance.userName); + expectType(AutoTypeModelInstance.favoritDrink); + expectType(AutoTypeModelInstance.favoritColorMode); + expectType(AutoTypeModelInstance.unExistProperty); + expectType(AutoTypeModelInstance.description); + + // Document-Methods-tests + expectType>(new AutoTypedModel().instanceFn()); + } \ No newline at end of file diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 1c5c75a6701..29b1fe4ebb0 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -1,5 +1,7 @@ -import { Schema, Document, Model, Types, connection, model } from 'mongoose'; -import { expectError } from 'tsd'; +import { ObjectId } from 'bson'; +import { Schema, Document, Model, connection, model, Types } from 'mongoose'; +import { expectError, expectType } from 'tsd'; +import { AutoTypedSchemaType, autoTypedSchema } from './schema.test'; function conventionalSyntax(): void { interface ITest extends Document { @@ -40,9 +42,7 @@ function rawDocSyntax(): void { const Test = connection.model('Test', TestSchema); - const bar = (SomeModel: Model) => console.log(SomeModel); - - bar(Test); + expectType>(Test); const doc = new Test({ foo: '42' }); console.log(doc.foo); @@ -83,7 +83,7 @@ async function insertManyTest() { }); const res = await Test.insertMany([{ foo: 'bar' }], { rawResult: true }); - const ids: Types.ObjectId[] = Object.values(res.insertedIds); + expectType(res.insertedIds[0]); } function schemaStaticsWithoutGenerics() { @@ -139,13 +139,17 @@ async function gh10359() { lastName: string; } - async function foo(model: Model): Promise { - const doc: T | null = await model.findOne({ groupId: 'test' }).lean().exec(); + async function foo(model: Model) { + const doc = await model.findOne({ groupId: 'test' }).lean().exec(); + expectType(doc?.firstName); + expectType(doc?.lastName); + expectType(doc?._id); + expectType(doc?.groupId); return doc; } const UserModel = model('gh10359', new Schema({ firstName: String, lastName: String, groupId: String })); - const u: User | null = await foo(UserModel); + foo(UserModel); } const ExpiresSchema = new Schema({ @@ -234,4 +238,34 @@ function bulkWrite() { } ]; M.bulkWrite(ops); +} + +export function autoTypedModel() { + const AutoTypedSchema = autoTypedSchema(); + const AutoTypedModel = model('AutoTypeModel', AutoTypedSchema); + + (async() => { + // Model-functions-test + // Create should works with arbitrary objects. + const randomObject = await AutoTypedModel.create({ unExistKey: 'unExistKey', description: 'st' }); + expectType(randomObject.unExistKey); + expectType(randomObject.userName); + + const testDoc1 = await AutoTypedModel.create({ userName: 'M0_0a' }); + expectType(testDoc1.userName); + expectType(testDoc1.description); + + const testDoc2 = await AutoTypedModel.insertMany([{ userName: 'M0_0a' }]); + expectType(testDoc2[0].userName); + expectType(testDoc2[0]?.description); + + const testDoc3 = await AutoTypedModel.findOne({ userName: 'M0_0a' }); + expectType(testDoc3?.userName); + expectType(testDoc3?.description); + + // Model-statics-functions-test + expectType>(AutoTypedModel.staticFn()); + + })(); + return AutoTypedModel; } \ No newline at end of file diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts index 46e2021946e..a3495d2f4c0 100644 --- a/test/types/populate.test.ts +++ b/test/types/populate.test.ts @@ -72,6 +72,7 @@ const Story = model('Story', storySchema); await story.populate('author'); await story.populate({ path: 'fans' }); + await story.populate({ path: 'fans', model: Person }); await story.populate(['author']); await story.populate([{ path: 'fans' }]); await story.populate(['author', { path: 'fans' }]); diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 1f029a99158..5d5ad58948f 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -1,6 +1,8 @@ import { HydratedDocument, Schema, model, Document, Types, Query, Model, QueryWithHelpers, PopulatedDoc, FilterQuery, UpdateQuery } from 'mongoose'; import { ObjectId } from 'mongodb'; import { expectError, expectType } from 'tsd'; +import { autoTypedModel } from './models.test'; +import { AutoTypedSchemaType } from './schema.test'; interface QueryHelpers { _byName(this: QueryWithHelpers, name: string): QueryWithHelpers, ITest, QueryHelpers>; @@ -161,10 +163,10 @@ function testGenericQuery(): void { function eachAsync(): void { Test.find().cursor().eachAsync((doc) => { - expectType<(ITest & { _id: any; })>(doc); + expectType<(ITest & { _id: Types.ObjectId; })>(doc); }); Test.find().cursor().eachAsync((docs) => { - expectType<(ITest & { _id: any; })[]>(docs); + expectType<(ITest & { _id: Types.ObjectId; })[]>(docs); }, { batchSize: 2 }); } @@ -286,4 +288,10 @@ async function gh11602(): Promise { expectError(updateResult.lastErrorObject?.modifiedCount); expectType(updateResult.lastErrorObject?.updatedExisting); expectType(updateResult.lastErrorObject?.upserted); -} \ No newline at end of file +} + +function autoTypedQuery() { + const AutoTypedModel = autoTypedModel(); + const query = AutoTypedModel.find(); + expectType(AutoTypedModel.find().byUserName('')); +} diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index bcc13d2484b..a510596d045 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1,5 +1,5 @@ -import { Schema, Document, SchemaDefinition, Model, Types } from 'mongoose'; -import { expectType, expectNotType, expectError } from 'tsd'; +import { Schema, Document, SchemaDefinition, Model, Types, InferSchemaType, SchemaType, Query, HydratedDocument } from 'mongoose'; +import { expectType, expectError, expectAssignable } from 'tsd'; enum Genre { Action, @@ -318,4 +318,197 @@ function gh10900(): void { const patientSchema = new Schema({ menuStatus: { type: Schema.Types.Mixed, default: {} } }); -} \ No newline at end of file +} + +export function autoTypedSchema() { + // Test auto schema type obtaining with all possible path types. + + class Int8 extends SchemaType { + constructor(key, options) { + super(key, options, 'Int8'); + } + cast(val) { + let _val = Number(val); + if (isNaN(_val)) { + throw new Error('Int8: ' + val + ' is not a number'); + } + _val = Math.round(_val); + if (_val < -0x80 || _val > 0x7F) { + throw new Error('Int8: ' + val + + ' is outside of the range of valid 8-bit ints'); + } + return _val; + } + } + + type TestSchemaType = { + string1?: string; + string2?: string; + string3?: string; + string4?: string; + number1?: number; + number2?: number; + number3?: number; + number4?: number; + date1?: Date; + date2?: Date; + date3?: Date; + date4?: Date; + buffer1?: Buffer; + buffer2?: Buffer; + buffer3?: Buffer; + buffer4?: Buffer; + boolean1?: boolean; + boolean2?: boolean; + boolean3?: boolean; + boolean4?: boolean; + mixed1?: any; + mixed2?: any; + mixed3?: any; + objectId1?: Schema.Types.ObjectId; + objectId2?: Schema.Types.ObjectId; + objectId3?: Schema.Types.ObjectId; + customSchema?: Int8; + map1?: Map; + map2?: Map; + array1?: string[]; + array2?: any[]; + array3?: any[]; + array4?: any[]; + array5?: any[]; + decimal1?: Schema.Types.Decimal128; + decimal2?: Schema.Types.Decimal128; + decimal3?: Schema.Types.Decimal128; + }; + + const TestSchema = new Schema({ + string1: String, + string2: 'String', + string3: 'string', + string4: Schema.Types.String, + number1: Number, + number2: 'Number', + number3: 'number', + number4: Schema.Types.Number, + date1: Date, + date2: 'Date', + date3: 'date', + date4: Schema.Types.Date, + buffer1: Buffer, + buffer2: 'Buffer', + buffer3: 'buffer', + buffer4: Schema.Types.Buffer, + boolean1: Boolean, + boolean2: 'Boolean', + boolean3: 'boolean', + boolean4: Schema.Types.Boolean, + mixed1: Object, + mixed2: {}, + mixed3: Schema.Types.Mixed, + objectId1: Schema.Types.ObjectId, + objectId2: 'ObjectId', + objectId3: 'objectId', + customSchema: Int8, + map1: { type: Map, of: String }, + map2: { type: Map, of: Number }, + array1: [String], + array2: Array, + array3: [Schema.Types.Mixed], + array4: [{}], + array5: [], + decimal1: Schema.Types.Decimal128, + decimal2: 'Decimal128', + decimal3: 'decimal128' + }); + + type InferredTestSchemaType = InferSchemaType; + + expectType({} as InferredTestSchemaType); + + const SchemaWithCustomTypeKey = new Schema({ + name: { + customTypeKey: String, + required: true + } + }, { + typeKey: 'customTypeKey' + }); + + expectType({} as InferSchemaType['name']); + + const AutoTypedSchema = new Schema({ + userName: { + type: String, + required: [true, 'userName is required'] + }, + description: String, + nested: new Schema({ + age: { + type: Number, + required: true + }, + hobby: { + type: String, + required: false + } + }), + favoritDrink: { + type: String, + enum: ['Coffee', 'Tea'] + }, + favoritColorMode: { + type: String, + enum: { + values: ['dark', 'light'], + message: '{VALUE} is not supported' + }, + required: true + } + }, { + statics: { + staticFn() { + expectType>(this); + return 'Returned from staticFn' as const; + } + }, + methods: { + instanceFn() { + expectType>(this); + return 'Returned from DocumentInstanceFn' as const; + } + }, + query: { + byUserName(userName) { + expectAssignable>(this); + return this.where({ userName }); + } + } + }); + + type InferredSchemaType = InferSchemaType; + + expectType({} as InferredSchemaType); + + expectError({} as InferredSchemaType); + + return AutoTypedSchema; +} + +export type AutoTypedSchemaType = { + schema: { + userName: string; + description?: string; + nested?: { + age: number; + hobby?: string + }, + favoritDrink?: 'Tea' | 'Coffee', + favoritColorMode: 'dark' | 'light' + } + , statics: { + staticFn: () => 'Returned from staticFn' + }, + methods: { + instanceFn: () => 'Returned from DocumentInstanceFn' + }, +}; \ No newline at end of file diff --git a/test/types/utility.test.ts b/test/types/utility.test.ts new file mode 100644 index 00000000000..03dc3d7e9af --- /dev/null +++ b/test/types/utility.test.ts @@ -0,0 +1,13 @@ +import { MergeType } from 'mongoose'; +import { expectType } from 'tsd'; + +type A = { a: string, c: number }; +type B = { a: number, b: string }; + +expectType({} as MergeType['a']); +expectType({} as MergeType['b']); +expectType({} as MergeType['c']); + +expectType({} as MergeType['a']); +expectType({} as MergeType['b']); +expectType({} as MergeType['c']); diff --git a/tsconfig.json b/tsconfig.json index 98bd39acf40..10f087f4bf2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "strict": true, "strictNullChecks": true, "paths": { "mongoose" : ["./types/index.d.ts"] diff --git a/types/document.d.ts b/types/document.d.ts index 157ef53a6ab..2316e3e5d22 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -196,8 +196,8 @@ declare module 'mongoose' { /** Populates document references. */ populate(path: string | PopulateOptions | (string | PopulateOptions)[]): Promise; populate(path: string | PopulateOptions | (string | PopulateOptions)[], callback: Callback): void; - populate(path: string, select?: string | AnyObject, model?: Model, match?: AnyObject, options?: PopulateOptions): Promise; - populate(path: string, select?: string | AnyObject, model?: Model, match?: AnyObject, options?: PopulateOptions, callback?: Callback): void; + populate(path: string, select?: string | AnyObject, model?: Model, match?: AnyObject, options?: PopulateOptions): Promise; + populate(path: string, select?: string | AnyObject, model?: Model, match?: AnyObject, options?: PopulateOptions, callback?: Callback): void; /** Gets _id(s) used during population of the given `path`. If the path was not populated, returns `undefined`. */ populated(path: string): any; diff --git a/types/index.d.ts b/types/index.d.ts index 3695eadcd9b..46e4cbc22f5 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -19,6 +19,7 @@ /// /// /// +/// declare class NativeDate extends global.Date { } @@ -61,10 +62,18 @@ declare module 'mongoose' { /* ! ignore */ export type CompileModelOptions = { overwriteModels?: boolean, connection?: Connection }; + export function model( + name: string, + schema?: TSchema, + collection?: string, + options?: CompileModelOptions + ): Model, ObtainSchemaGeneric, ObtainSchemaGeneric, {}, TSchema> & ObtainSchemaGeneric; + export function model(name: string, schema?: Schema | Schema, collection?: string, options?: CompileModelOptions): Model; + export function model( name: string, - schema?: Schema, + schema?: Schema, collection?: string, options?: CompileModelOptions ): U; @@ -91,7 +100,16 @@ declare module 'mongoose' { export interface AnyObject { [k: string]: any } - export type Require_id = T extends { _id?: any } ? (T & { _id: T['_id'] }) : (T & { _id: Types.ObjectId }); + + export type Require_id = T extends { _id?: infer U } + ? U extends any + ? (T & { _id: Types.ObjectId }) + : T & Required<{ _id: U }> + : T & { _id: Types.ObjectId }; + + export type RequireOnlyTypedId = T extends { _id?: infer U; } + ? Required<{ _id: U }> + : { _id: Types.ObjectId }; export type HydratedDocument = DocType extends Document ? Require_id : (Document & Require_id & TVirtuals & TMethodsAndOverrides); @@ -120,14 +138,18 @@ declare module 'mongoose' { useProjection?: boolean; } - export class Schema, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = any> extends events.EventEmitter { + export class Schema, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = any, + TStaticMethods = {}, + TPathTypeKey extends TypeKeyBaseType = DefaultTypeKey, + DocType extends ObtainDocumentType = ObtainDocumentType> + extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition>, options?: SchemaOptions); + constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ - add(obj: SchemaDefinition> | Schema, prefix?: string): this; + add(obj: SchemaDefinition> | Schema, prefix?: string): this; /** * Array of child schemas (from document arrays and single nested subdocs) @@ -180,7 +202,7 @@ declare module 'mongoose' { methods: { [F in keyof TInstanceMethods]: TInstanceMethods[F] } & AnyObject; /** The original object passed to the schema constructor */ - obj: SchemaDefinition>; + obj: SchemaDefinition>; /** Gets/sets schema paths. */ path(path: string): ResultType; diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts new file mode 100644 index 00000000000..cc8c89e5cab --- /dev/null +++ b/types/inferschematype.d.ts @@ -0,0 +1,155 @@ +import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions, TypeKeyBaseType } from 'mongoose'; + +declare module 'mongoose' { + /** + * @summary Obtains document schema type. + * @description Obtains document schema type from document Definition OR returns enforced schema type if it's provided. + * @param {DocDefinition} DocDefinition A generic equals to the type of document definition "provided in as first parameter in Schema constructor". + * @param {EnforcedDocType} EnforcedDocType A generic type enforced by user "provided before schema constructor". + * @param {TypeKey} TypeKey A generic of literal string type. + */ + type ObtainDocumentType = + IsItRecordAndNotAny extends true ? EnforcedDocType : { + [K in keyof (RequiredPaths & + OptionalPaths)]: ObtainDocumentPathType; + }; + + /** + * @summary Obtains document schema type from Schema instance. + * @param {SchemaType} SchemaType A generic of schema type instance. + * @example + * const userSchema = new Schema({userName:String}); + * type UserType = InferSchemaType; + * // result + * type UserType = {userName?: string} + */ + type InferSchemaType = ObtainSchemaGeneric ; + + /** + * @summary Obtains schema Generic type by using generic alias. + * @param {TSchema} TSchema A generic of schema type instance. + * @param {alias} alias Targeted generic alias. + */ + type ObtainSchemaGeneric = + TSchema extends Schema + ? { + EnforcedDocType: EnforcedDocType; + M: M; + TInstanceMethods: TInstanceMethods; + TQueryHelpers: TQueryHelpers; + TVirtuals: TVirtuals; + TStaticMethods: TStaticMethods; + TPathTypeKey: TPathTypeKey; + DocType: DocType; + }[alias] + : unknown; +} +/** + * @summary Checks if a type is "Record" or "any". + * @description It Helps to check if user has provided schema type "EnforcedDocType" + * @param {T} T A generic type to be checked. + * @returns true if {@link T} is Record OR false if {@link T} is of any type. + */ +type IsItRecordAndNotAny = IfEquals ? true : false>; + +/** + * @summary Checks if two types are identical. + * @param {T} T The first type to be compared with {@link U}. + * @param {U} U The seconde type to be compared with {@link T}. + * @param {Y} Y A type to be returned if {@link T} & {@link U} are identical. + * @param {N} N A type to be returned if {@link T} & {@link U} are not identical. + */ +type IfEquals = + (() => G extends T ? 1 : 0) extends + (() => G extends U ? 1 : 0) ? Y : N; + +/** + * @summary Required path base type. + * @description It helps to check whereas if a path is required OR optional. + */ +type RequiredPathBaseType = { required: true | [true, string | undefined] }; + +/** + * @summary Path base type defined by using TypeKey + * @description It helps to check if a path is defined by TypeKey OR not. + * @param {TypeKey} TypeKey A literal string refers to path type property key. + */ +type PathWithTypePropertyBaseType = { [k in TypeKey]: any }; + +/** + * @summary A Utility to obtain schema's required path keys. + * @param {T} T A generic refers to document definition. + * @returns required paths keys of document definition. + */ +type RequiredPathKeys = { + [K in keyof T]: T[K] extends RequiredPathBaseType ? IfEquals : never; +}[keyof T]; + +/** + * @summary A Utility to obtain schema's required paths. + * @param {T} T A generic refers to document definition. + * @returns a record contains required paths with the corresponding type. + */ +type RequiredPaths = { + [K in RequiredPathKeys]: T[K]; +}; + +/** + * @summary A Utility to obtain schema's optional path keys. + * @param {T} T A generic refers to document definition. + * @returns optional paths keys of document definition. + */ +type OptionalPathKeys = { + [K in keyof T]: T[K] extends RequiredPathBaseType ? never : K; +}[keyof T]; + +/** + * @summary A Utility to obtain schema's optional paths. + * @param {T} T A generic refers to document definition. + * @returns a record contains optional paths with the corresponding type. + */ +type OptionalPaths = { + [K in OptionalPathKeys]?: T[K]; +}; + +/** + * @summary Obtains schema Path type. + * @description Obtains Path type by calling {@link ResolvePathType} OR by calling {@link InferSchemaType} if path of schema type. + * @param {PathValueType} PathValueType Document definition path type. + * @param {TypeKey} TypeKey A generic refers to document definition. + */ +type ObtainDocumentPathType = PathValueType extends Schema + ? InferSchemaType + : ResolvePathType< + PathValueType extends PathWithTypePropertyBaseType ? PathValueType[TypeKey] : PathValueType, + PathValueType extends PathWithTypePropertyBaseType ? Omit : {} + >; + +/** + * @param {T} T A generic refers to string path enums. + * @returns Path enum values type as literal strings or string. + */ +type PathEnumOrString['enum']> = T extends (infer E)[] ? E : T extends { values: any } ? PathEnumOrString : string; + +/** + * @summary Resolve path type by returning the corresponding type. + * @param {PathValueType} PathValueType Document definition path type. + * @param {Options} Options Document definition path options except path type. + * @returns Number, "Number" or "number" will be resolved to string type. + */ +type ResolvePathType = {}> = + PathValueType extends (infer Item)[] ? IfEquals>[] : + PathValueType extends StringConstructor | 'string' | 'String' | typeof Schema.Types.String ? PathEnumOrString : + PathValueType extends NumberConstructor | 'number' | 'Number' | typeof Schema.Types.Number ? number : + PathValueType extends DateConstructor | 'date' | 'Date' | typeof Schema.Types.Date ? Date : + PathValueType extends BufferConstructor | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : + PathValueType extends BooleanConstructor | 'boolean' | 'Boolean' | typeof Schema.Types.Boolean ? boolean : + PathValueType extends 'objectId' | 'ObjectId' | typeof Schema.Types.ObjectId ? Schema.Types.ObjectId : + PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Schema.Types.Decimal128 : + PathValueType extends MapConstructor ? Map> : + PathValueType extends ArrayConstructor ? any[] : + PathValueType extends typeof Schema.Types.Mixed ? any: + IfEquals extends true ? any: + IfEquals extends true ? any: + PathValueType extends typeof SchemaType ? PathValueType['prototype'] : + unknown; \ No newline at end of file diff --git a/types/models.d.ts b/types/models.d.ts index 3de92a2823f..d15675dbb1b 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -114,12 +114,12 @@ declare module 'mongoose' { } const Model: Model; - interface Model extends + interface Model extends NodeJS.EventEmitter, AcceptsDiscriminator, IndexManager, SessionStarter { - new & AnyObject>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; + new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument, TMethodsAndOverrides, TVirtuals> & ObtainSchemaGeneric; aggregate(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback): Aggregate>; aggregate(pipeline: PipelineStage[], callback?: Callback): Aggregate>; @@ -128,27 +128,27 @@ declare module 'mongoose' { base: Mongoose; /** - * If this is a discriminator model, `baseModelName` is the name of - * the base model. - */ + * If this is a discriminator model, `baseModelName` is the name of + * the base model. + */ baseModelName: string | undefined; /** - * Sends multiple `insertOne`, `updateOne`, `updateMany`, `replaceOne`, - * `deleteOne`, and/or `deleteMany` operations to the MongoDB server in one - * command. This is faster than sending multiple independent operations (e.g. - * if you use `create()`) because with `bulkWrite()` there is only one network - * round trip to the MongoDB server. - */ + * Sends multiple `insertOne`, `updateOne`, `updateMany`, `replaceOne`, + * `deleteOne`, and/or `deleteMany` operations to the MongoDB server in one + * command. This is faster than sending multiple independent operations (e.g. + * if you use `create()`) because with `bulkWrite()` there is only one network + * round trip to the MongoDB server. + */ bulkWrite(writes: Array, options: mongodb.BulkWriteOptions & MongooseBulkWriteOptions, callback: Callback): void; bulkWrite(writes: Array, callback: Callback): void; bulkWrite(writes: Array, options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions): Promise; /** - * Sends multiple `save()` calls in a single `bulkWrite()`. This is faster than - * sending multiple `save()` calls because with `bulkSave()` there is only one - * network round trip to the MongoDB server. - */ + * Sends multiple `save()` calls in a single `bulkWrite()`. This is faster than + * sending multiple `save()` calls because with `bulkSave()` there is only one + * network round trip to the MongoDB server. + */ bulkSave(documents: Array, options?: mongodb.BulkWriteOptions): Promise; /** Collection the model uses. */ @@ -163,21 +163,17 @@ declare module 'mongoose' { countDocuments(callback?: Callback): QueryWithHelpers, TQueryHelpers, T>; /** Creates a new document or documents */ - create(docs: (AnyKeys | AnyObject)[], options?: SaveOptions): Promise[]>; - create(docs: (AnyKeys | AnyObject)[], callback: Callback[]>): void; - create(doc: AnyKeys | AnyObject): Promise>; - create(doc: AnyKeys | AnyObject, callback: Callback>): void; - create>(docs: DocContents[], options?: SaveOptions): Promise[]>; - create>(docs: DocContents[], callback: Callback[]>): void; - create>(doc: DocContents): Promise>; - create>(...docs: DocContents[]): Promise[]>; - create>(doc: DocContents, callback: Callback>): void; + create(docs: Array, options?: SaveOptions): Promise, RequireOnlyTypedId>, TMethodsAndOverrides, TVirtuals>[]>; + create(docs: Array, callback: Callback, RequireOnlyTypedId>, TMethodsAndOverrides, TVirtuals>[]>): void; + create(doc: DocContents | T): Promise, RequireOnlyTypedId>, TMethodsAndOverrides, TVirtuals>>; + create(...docs: Array): Promise, RequireOnlyTypedId>, TMethodsAndOverrides, TVirtuals>[]>; + create(doc: T | DocContents, callback: Callback, RequireOnlyTypedId>, TMethodsAndOverrides, TVirtuals>>): void; /** - * Create the collection for this model. By default, if no indexes are specified, - * mongoose will not create the collection for the model until any documents are - * created. Use this method to create the collection explicitly. - */ + * Create the collection for this model. By default, if no indexes are specified, + * mongoose will not create the collection for the model until any documents are + * created. Use this method to create the collection explicitly. + */ createCollection(options: mongodb.CreateCollectionOptions & Pick | null, callback: Callback>): void; createCollection(callback: Callback>): void; createCollection(options?: mongodb.CreateCollectionOptions & Pick): Promise>; @@ -186,34 +182,34 @@ declare module 'mongoose' { db: Connection; /** - * Deletes all of the documents that match `conditions` from the collection. - * Behaves like `remove()`, but deletes all documents that match `conditions` - * regardless of the `single` option. - */ + * Deletes all of the documents that match `conditions` from the collection. + * Behaves like `remove()`, but deletes all documents that match `conditions` + * regardless of the `single` option. + */ deleteMany(filter?: FilterQuery, options?: QueryOptions, callback?: CallbackWithoutResult): QueryWithHelpers, TQueryHelpers, T>; deleteMany(filter: FilterQuery, callback: CallbackWithoutResult): QueryWithHelpers, TQueryHelpers, T>; deleteMany(callback: CallbackWithoutResult): QueryWithHelpers, TQueryHelpers, T>; /** - * Deletes the first document that matches `conditions` from the collection. - * Behaves like `remove()`, but deletes at most one document regardless of the - * `single` option. - */ + * Deletes the first document that matches `conditions` from the collection. + * Behaves like `remove()`, but deletes at most one document regardless of the + * `single` option. + */ deleteOne(filter?: FilterQuery, options?: QueryOptions, callback?: CallbackWithoutResult): QueryWithHelpers, TQueryHelpers, T>; deleteOne(filter: FilterQuery, callback: CallbackWithoutResult): QueryWithHelpers, TQueryHelpers, T>; deleteOne(callback: CallbackWithoutResult): QueryWithHelpers, TQueryHelpers, T>; /** - * Event emitter that reports any errors that occurred. Useful for global error - * handling. - */ + * Event emitter that reports any errors that occurred. Useful for global error + * handling. + */ events: NodeJS.EventEmitter; /** - * Finds a single document by its _id field. `findById(id)` is almost* - * equivalent to `findOne({ _id: id })`. If you want to query by a document's - * `_id`, use `findById()` instead of `findOne()`. - */ + * Finds a single document by its _id field. `findById(id)` is almost* + * equivalent to `findOne({ _id: id })`. If you want to query by a document's + * `_id`, use `findById()` instead of `findOne()`. + */ findById>( id: any, projection?: ProjectionType | null, @@ -244,28 +240,37 @@ declare module 'mongoose' { ): QueryWithHelpers; /** - * Shortcut for creating a new Document from existing raw data, pre-saved in the DB. - * The document returned has no paths marked as modified initially. - */ + * Shortcut for creating a new Document from existing raw data, pre-saved in the DB. + * The document returned has no paths marked as modified initially. + */ hydrate(obj: any): HydratedDocument; /** - * This function is responsible for building [indexes](https://docs.mongodb.com/manual/indexes/), - * unless [`autoIndex`](http://mongoosejs.com/docs/guide.html#autoIndex) is turned off. - * Mongoose calls this function automatically when a model is created using - * [`mongoose.model()`](/docs/api.html#mongoose_Mongoose-model) or - * [`connection.model()`](/docs/api.html#connection_Connection-model), so you - * don't need to call it. - */ + * This function is responsible for building [indexes](https://docs.mongodb.com/manual/indexes/), + * unless [`autoIndex`](http://mongoosejs.com/docs/guide.html#autoIndex) is turned off. + * Mongoose calls this function automatically when a model is created using + * [`mongoose.model()`](/docs/api.html#mongoose_Mongoose-model) or + * [`connection.model()`](/docs/api.html#connection_Connection-model), so you + * don't need to call it. + */ init(callback?: CallbackWithoutResult): Promise>; /** Inserts one or more new documents as a single `insertMany` call to the MongoDB server. */ - insertMany(docs: Array | AnyObject>, options: InsertManyOptions & { rawResult: true }): Promise>; - insertMany(docs: Array | AnyObject>, options?: InsertManyOptions): Promise>>; - insertMany(doc: AnyKeys | AnyObject, options: InsertManyOptions & { rawResult: true }): Promise>; - insertMany(doc: AnyKeys | AnyObject, options?: InsertManyOptions): Promise[]>; - insertMany(doc: AnyKeys | AnyObject, options?: InsertManyOptions, callback?: Callback[] | InsertManyResult>): void; - insertMany(docs: Array | AnyObject>, options?: InsertManyOptions, callback?: Callback> | InsertManyResult>): void; + insertMany(docs: Array, options: InsertManyOptions & { lean: true; }, callback: Callback, RequireOnlyTypedId>>>): void; + insertMany(docs: Array, options: InsertManyOptions & { rawResult: true; }, callback: Callback>): void; + insertMany(docs: Array, callback: Callback, RequireOnlyTypedId>, TMethodsAndOverrides, TVirtuals>>>): void; + insertMany(doc: DocContents, options: InsertManyOptions & { lean: true; }, callback: Callback, RequireOnlyTypedId>>>): void; + insertMany(doc: DocContents, options: InsertManyOptions & { rawResult: true; }, callback: Callback>): void; + insertMany(doc: DocContents, options: InsertManyOptions & { lean?: false | undefined }, callback: Callback, RequireOnlyTypedId>, TMethodsAndOverrides, TVirtuals>>>): void; + insertMany(doc: DocContents, callback: Callback, RequireOnlyTypedId>, TMethodsAndOverrides, TVirtuals>>>): void; + + insertMany(docs: Array, options: InsertManyOptions & { lean: true; }): Promise, RequireOnlyTypedId>>>; + insertMany(docs: Array, options: InsertManyOptions & { rawResult: true; }): Promise>; + insertMany(docs: Array): Promise, RequireOnlyTypedId>, TMethodsAndOverrides, TVirtuals>>>; + insertMany(doc: DocContents, options: InsertManyOptions & { lean: true; }): Promise, RequireOnlyTypedId>>>; + insertMany(doc: DocContents, options: InsertManyOptions & { rawResult: true; }): Promise>; + insertMany(doc: DocContents, options: InsertManyOptions): Promise, RequireOnlyTypedId>, TMethodsAndOverrides, TVirtuals>>>; + insertMany(doc: DocContents): Promise, RequireOnlyTypedId>, TMethodsAndOverrides, TVirtuals>>>; /** The name of the model */ modelName: string; @@ -301,21 +306,31 @@ declare module 'mongoose' { estimatedDocumentCount(options?: QueryOptions, callback?: Callback): QueryWithHelpers, TQueryHelpers, T>; /** - * Returns a document with its `_id` if at least one document exists in the database that matches - * the given `filter`, and `null` otherwise. - */ + * Returns a document with its `_id` if at least one document exists in the database that matches + * the given `filter`, and `null` otherwise. + */ exists(filter: FilterQuery, callback: Callback, '_id'> | null>): QueryWithHelpers, '_id'> | null, HydratedDocument, TQueryHelpers, T>; exists(filter: FilterQuery): QueryWithHelpers, '_id'> | null, HydratedDocument, TQueryHelpers, T>; /** Creates a `find` query: gets a list of documents that match `filter`. */ - find>(callback?: Callback): QueryWithHelpers, ResultDoc, TQueryHelpers, T>; find>( filter: FilterQuery, - projection?: ProjectionType | null, + projection: ProjectionType | null, + options: QueryOptions | null, + callback?: Callback + ): QueryWithHelpers, ResultDoc, TQueryHelpers, T>; + find>( + filter: FilterQuery, + projection: ProjectionType | null, + callback?: Callback + ): QueryWithHelpers, ResultDoc, TQueryHelpers, T>; + find>( + filter: FilterQuery, + callback?: Callback + ): QueryWithHelpers, ResultDoc, TQueryHelpers, T>; + find>( callback?: Callback ): QueryWithHelpers, ResultDoc, TQueryHelpers, T>; - find>(filter: FilterQuery, callback?: Callback): QueryWithHelpers, ResultDoc, TQueryHelpers, T>; - find>(filter: FilterQuery, projection?: ProjectionType | null, options?: QueryOptions | null, callback?: Callback): QueryWithHelpers, ResultDoc, TQueryHelpers, T>; /** Creates a `findByIdAndDelete` query, filtering by the given `_id`. */ findByIdAndDelete>(id?: mongodb.ObjectId | any, options?: QueryOptions | null, callback?: (err: CallbackError, doc: ResultDoc | null, res: any) => void): QueryWithHelpers; @@ -385,9 +400,9 @@ declare module 'mongoose' { schema: Schema; /** - * @deprecated use `updateOne` or `updateMany` instead. - * Creates a `update` query: updates one or many documents that match `filter` with `update`, based on the `multi` option. - */ + * @deprecated use `updateOne` or `updateMany` instead. + * Creates a `update` query: updates one or many documents that match `filter` with `update`, based on the `multi` option. + */ update>( filter?: FilterQuery, update?: UpdateQuery | UpdateWithAggregationPipeline, diff --git a/types/query.d.ts b/types/query.d.ts index ab7174f11b0..7062c52f5c7 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -1,7 +1,7 @@ declare module 'mongoose' { import mongodb = require('mongodb'); - type ApplyBasicQueryCasting = T | T[] | any; + export type ApplyBasicQueryCasting = T extends any[] ? T[0] & defaultT: defaultT; type Condition = ApplyBasicQueryCasting | QuerySelector>; type _FilterQuery = { diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 505077c31bd..689383a3f2d 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -7,7 +7,10 @@ declare module 'mongoose' { currentTime?: () => (NativeDate | number); } - interface SchemaOptions { + type TypeKeyBaseType = string; + + type DefaultTypeKey = 'type'; + interface SchemaOptions { /** * By default, Mongoose's init() function creates all the indexes defined in your model's schema by * calling Model.createIndexes() after you successfully connect to MongoDB. If you want to disable @@ -136,7 +139,7 @@ declare module 'mongoose' { * type declaration. However, for applications like geoJSON, the 'type' property is important. If you want to * control which key mongoose uses to find type declarations, set the 'typeKey' schema option. */ - typeKey?: string; + typeKey?: PathTypeKey; /** * By default, documents are automatically validated before they are saved to the database. This is to @@ -183,6 +186,21 @@ declare module 'mongoose' { * You can suppress the warning by setting { supressReservedKeysWarning: true } schema options. Keep in mind that this * can break plugins that rely on these reserved names. */ - supressReservedKeysWarning?: boolean + supressReservedKeysWarning?: boolean, + + /** + * Model Statics methods. + */ + statics?: Record, ...args: any) => unknown> | StaticMethods, + + /** + * Document instance methods. + */ + methods?: Record, ...args: any) => unknown> | InstanceMethods, + + /** + * Query helper functions + */ + query?: Record>(this: T, ...args: any) => T> | QueryHelpers, } } \ No newline at end of file diff --git a/types/utility.d.ts b/types/utility.d.ts index efa98ebe580..997dcef69bf 100644 --- a/types/utility.d.ts +++ b/types/utility.d.ts @@ -10,4 +10,6 @@ declare module 'mongoose' { ? T : Omit & U; + type MergeType, B extends Record> = Omit & B; + }