diff --git a/CHANGELOG.md b/CHANGELOG.md index 8742572d1a9..57748baf5fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +6.2.8 / 2022-03-22 +================== + * fix(document): handle casting array of spread docs #11522 + * fix(document): avoid setting nested properties on top-level document when initing with strict: false #11526 + * fix(document): correctly handle deeply nested subdocuments when getting paths to validate #11501 + * fix(types): avoid making TInstanceMethods any by default leading to `this = any` in middleware #11435 + * fix(types): allow defining array default if using Types.Array<> in document interface #11391 + * docs(migrating_to_6): describe breaking change in Mongoose 6 about default query populate model #11289 + * docs(middleware): fix typo #11537 [x1489](https://github.com/x1489) + 6.2.7 / 2022-03-16 ================== * perf(document): avoid running validation on every array element if there's no validators to run #11380 diff --git a/lib/document.js b/lib/document.js index 0e4e1c0a2f7..abd1d62903c 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2421,6 +2421,7 @@ Document.prototype.validate = function(pathsToValidate, options, callback) { } this.$__validate(pathsToValidate, options, (error) => { + this.$__.validating = null; this.$op = null; cb(error); }); diff --git a/lib/index.js b/lib/index.js index 47d6f7dabbd..01538b05dd8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -24,7 +24,6 @@ const legacyPluralize = require('./helpers/pluralize'); const utils = require('./utils'); const pkg = require('../package.json'); const cast = require('./cast'); -const clearValidating = require('./plugins/clearValidating'); const removeSubdocs = require('./plugins/removeSubdocs'); const saveSubdocs = require('./plugins/saveSubdocs'); const trackTransaction = require('./plugins/trackTransaction'); @@ -107,8 +106,7 @@ function Mongoose(options) { [validateBeforeSave, { deduplicate: true }], [shardingPlugin, { deduplicate: true }], [removeSubdocs, { deduplicate: true }], - [trackTransaction, { deduplicate: true }], - [clearValidating, { deduplicate: true }] + [trackTransaction, { deduplicate: true }] ] }); } diff --git a/lib/model.js b/lib/model.js index fda0e476525..30413b7a76c 100644 --- a/lib/model.js +++ b/lib/model.js @@ -245,8 +245,7 @@ function _applyCustomWhere(doc, where) { */ Model.prototype.$__handleSave = function(options, callback) { - const _this = this; - let saveOptions = {}; + const saveOptions = {}; applyWriteConcern(this.$__schema, options); if (typeof options.writeConcern !== 'undefined') { @@ -274,14 +273,10 @@ Model.prototype.$__handleSave = function(options, callback) { if ('checkKeys' in options) { saveOptions.checkKeys = options.checkKeys; } - const session = this.$session(); if (!saveOptions.hasOwnProperty('session')) { - saveOptions.session = session; + saveOptions.session = this.$session(); } - if (Object.keys(saveOptions).length === 0) { - saveOptions = null; - } if (this.$isNew) { // send entire doc const obj = this.toObject(saveToObjectOptions); @@ -298,9 +293,9 @@ Model.prototype.$__handleSave = function(options, callback) { } this.$__version(true, obj); - this[modelCollectionSymbol].insertOne(obj, saveOptions, function(err, ret) { + this[modelCollectionSymbol].insertOne(obj, saveOptions, (err, ret) => { if (err) { - _setIsNew(_this, true); + _setIsNew(this, true); callback(err, null); return; @@ -308,64 +303,67 @@ Model.prototype.$__handleSave = function(options, callback) { callback(null, ret); }); + this.$__reset(); _setIsNew(this, false); // Make it possible to retry the insert this.$__.inserting = true; - } else { - // Make sure we don't treat it as a new object on error, - // since it already exists - this.$__.inserting = false; - - const delta = this.$__delta(); - if (delta) { - if (delta instanceof MongooseError) { - callback(delta); - return; - } - const where = this.$__where(delta[0]); - if (where instanceof MongooseError) { - callback(where); - return; - } + return; + } - _applyCustomWhere(this, where); - this[modelCollectionSymbol].updateOne(where, delta[1], saveOptions, (err, ret) => { - if (err) { - this.$__undoReset(); + // Make sure we don't treat it as a new object on error, + // since it already exists + this.$__.inserting = false; - callback(err); - return; - } - ret.$where = where; - callback(null, ret); - }); - } else { - const optionsWithCustomValues = Object.assign({}, options, saveOptions); - const where = this.$__where(); - if (this.$__schema.options.optimisticConcurrency) { - const key = this.$__schema.options.versionKey; - const val = this.$__getValue(key); - if (val != null) { - where[key] = val; - } - } - this.constructor.exists(where, optionsWithCustomValues) - .then(documentExists => { - const matchedCount = !documentExists ? 0 : 1; - callback(null, { $where: where, matchedCount }); - }) - .catch(callback); + const delta = this.$__delta(); + if (delta) { + if (delta instanceof MongooseError) { + callback(delta); return; } - // store the modified paths before the document is reset - this.$__.modifiedPaths = this.modifiedPaths(); - this.$__reset(); + const where = this.$__where(delta[0]); + if (where instanceof MongooseError) { + callback(where); + return; + } - _setIsNew(this, false); + _applyCustomWhere(this, where); + this[modelCollectionSymbol].updateOne(where, delta[1], saveOptions, (err, ret) => { + if (err) { + this.$__undoReset(); + + callback(err); + return; + } + ret.$where = where; + callback(null, ret); + }); + } else { + const optionsWithCustomValues = Object.assign({}, options, saveOptions); + const where = this.$__where(); + if (this.$__schema.options.optimisticConcurrency) { + const key = this.$__schema.options.versionKey; + const val = this.$__getValue(key); + if (val != null) { + where[key] = val; + } + } + this.constructor.exists(where, optionsWithCustomValues) + .then(documentExists => { + const matchedCount = !documentExists ? 0 : 1; + callback(null, { $where: where, matchedCount }); + }) + .catch(callback); + return; } + + // store the modified paths before the document is reset + this.$__.modifiedPaths = this.modifiedPaths(); + this.$__reset(); + + _setIsNew(this, false); }; /*! @@ -374,8 +372,8 @@ Model.prototype.$__handleSave = function(options, callback) { Model.prototype.$__save = function(options, callback) { this.$__handleSave(options, (error, result) => { - const hooks = this.$__schema.s.hooks; if (error) { + const hooks = this.$__schema.s.hooks; return hooks.execPost('save:error', this, [this], { error: error }, (error) => { callback(error, this); }); @@ -423,6 +421,7 @@ Model.prototype.$__save = function(options, callback) { this.$__undoReset(); error = new DocumentNotFoundError(result.$where, this.constructor.modelName, numAffected, result); + const hooks = this.$__schema.s.hooks; return hooks.execPost('save:error', this, [this], { error: error }, (error) => { callback(error, this); }); @@ -516,9 +515,9 @@ Model.prototype.save = function(options, fn) { this.$__.saveOptions = options; this.$__save(options, error => { - this.$__.saving = undefined; - delete this.$__.saveOptions; - delete this.$__.$versionError; + this.$__.saving = null; + this.$__.saveOptions = null; + this.$__.$versionError = null; this.$op = null; if (error) { diff --git a/lib/options/SchemaDateOptions.js b/lib/options/SchemaDateOptions.js index 09bf27f6adf..e1a1656d248 100644 --- a/lib/options/SchemaDateOptions.js +++ b/lib/options/SchemaDateOptions.js @@ -48,6 +48,13 @@ Object.defineProperty(SchemaDateOptions.prototype, 'max', opts); /** * If set, Mongoose creates a TTL index on this path. * + * mongo TTL index `expireAfterSeconds` value will take 'expires' value expressed in seconds. + * + * ####Example: + * + * const schema = new Schema({ "expireAt": { type: Date, expires: 11 } }); + * // if 'expireAt' is set, then document expires at expireAt + 11 seconds + * * @api public * @property expires * @memberOf SchemaDateOptions @@ -61,4 +68,4 @@ Object.defineProperty(SchemaDateOptions.prototype, 'expires', opts); * ignore */ -module.exports = SchemaDateOptions; \ No newline at end of file +module.exports = SchemaDateOptions; diff --git a/lib/schema.js b/lib/schema.js index f5a08244602..ba7a840df3b 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -106,9 +106,9 @@ 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.childSchemas = []; diff --git a/package.json b/package.json index 1600ad01a04..4c7193c1774 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.2.7", + "version": "6.2.8", "author": "Guillermo Rauch ", "keywords": [ "mongodb", @@ -20,7 +20,7 @@ "license": "MIT", "dependencies": { "bson": "^4.2.2", - "kareem": "2.3.4", + "kareem": "2.3.5", "mongodb": "4.3.1", "mpath": "0.8.4", "mquery": "4.0.2", diff --git a/test/document.test.js b/test/document.test.js index 5d630a7e334..10dde42e7b8 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -11198,4 +11198,12 @@ describe('document', function() { await doc.validate(); }); + + it('should give an instance function back rather than undefined (m0_0a)', function M0_0aModelJS() { + 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'); + }); + }); diff --git a/test/model.test.js b/test/model.test.js index 2cfce80e7b5..517e3f8a3a7 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -8485,4 +8485,12 @@ function pick(obj, keys) { } } return newObj; -} \ No newline at end of file +} + +describe('Check if statics functions that is supplied in schema option is availabe (m0_0a)', function() { + it('should give a static function back rather than undefined', function M0_0aModelJS() { + const testSchema = new mongoose.Schema({}, { statics: { staticFn() { return 'Returned from staticFn'; } } }); + const TestModel = mongoose.model('TestModel', testSchema); + assert.equal(TestModel.staticFn(), 'Returned from staticFn'); + }); +}); \ No newline at end of file diff --git a/test/types/create.test.ts b/test/types/create.test.ts index 4492d113fca..83372dd2b30 100644 --- a/test/types/create.test.ts +++ b/test/types/create.test.ts @@ -3,7 +3,7 @@ import { Schema, model, Document, Types } from 'mongoose'; const schema: Schema = new Schema({ name: { type: 'String' } }); interface ITest extends Document { - _id?: Types.ObjectId; + _id?: Types.ObjectId | string; name?: string; } diff --git a/test/types/document.test.ts b/test/types/document.test.ts index 28808731967..6aa4c8f1b2f 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -162,6 +162,12 @@ function gh11435() { const ItemSchema = new Schema({ name: String }); ItemSchema.pre('validate', function preValidate() { - expectType>(this.model('Item1')); + expectType>(this.model('Item1')); + }); + + const AutoTypedItemSchema = new Schema({ name: String }); + + AutoTypedItemSchema.pre('validate', function preValidate() { + expectType>(this.model('Item1')); }); } \ No newline at end of file diff --git a/test/types/middleware.test.ts b/test/types/middleware.test.ts index a5903e1c77b..bedd7899c9a 100644 --- a/test/types/middleware.test.ts +++ b/test/types/middleware.test.ts @@ -1,5 +1,5 @@ import { Schema, model, Model, Document, SaveOptions, Query, Aggregate, HydratedDocument, PreSaveMiddlewareFunction } from 'mongoose'; -import { expectError, expectType } from 'tsd'; +import { expectError, expectAssignable, expectNotType } from 'tsd'; interface ITest extends Document { name?: string; @@ -81,6 +81,19 @@ const Test = model('Test', schema); function gh11257(): void { schema.pre('save', { document: true }, function() { - expectType>(this); + expectAssignable>(this); + }); +} + +function gh11480(): void { + type IUserSchema = { + name: string; + } + + const UserSchema = new Schema({ name: { type: String } }); + + UserSchema.pre('save', function(next) { + expectNotType(this); + next(); }); } \ No newline at end of file diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 3878dbce1a3..3d20d9590c2 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -1,5 +1,6 @@ import { Schema, Document, Model, Types, connection, model } from 'mongoose'; -import { expectError } from 'tsd'; +import { expectAssignable, expectError, expectType } from 'tsd'; +import { M0_0aAutoTypedSchemaType, m0_0aSchema } from './schema.test'; function conventionalSyntax(): void { interface ITest extends Document { @@ -209,4 +210,50 @@ function inheritance() { Project.createCollection({ expires: '5 seconds' }); Project.createCollection({ expireAfterSeconds: 5 }); -expectError(Project.createCollection({ expireAfterSeconds: '5 seconds' })); \ No newline at end of file +expectError(Project.createCollection({ expireAfterSeconds: '5 seconds' })); + +export async function m0_0aModel() { + const AutoTypeSchema = m0_0aSchema(); + const AutoTypeModel = model('AutoTypeModel', AutoTypeSchema); + + /* -------------------------------------------------------------------------- */ + /* Model-functions-test */ + /* -------------------------------------------------------------------------- */ + // Create should works with arbitrary objects. + const randomObject = await AutoTypeModel.create({ unExistKey: 'unExistKey', description: 'st' }); + expectType(randomObject.unExistKey); + expectType(randomObject.userName); + + const testDoc1 = await AutoTypeModel.create({ userName: 'M0_0a' }); + expectType(testDoc1.userName); + expectType(testDoc1.description); + + const testDoc2 = await AutoTypeModel.findOne({ userName: 'M0_0a' }); + expectType(testDoc2?.userName); + expectType(testDoc2?.description); + + /* -------------------------------------------------------------------------- */ + /* Model-statics-functions-test */ + /* -------------------------------------------------------------------------- */ + + expectType>(AutoTypeModel.staticFn()); + + /* -------------------------------------------------------------------------- */ + /* Instance-Test */ + /* -------------------------------------------------------------------------- */ + + const AutoTypeModelInstance = new AutoTypeModel({}); + + expectType(AutoTypeModelInstance.userName); + expectType(AutoTypeModelInstance.favoritDrink); + expectType(AutoTypeModelInstance.favoritColorMode); + + /* -------------------------------------------------------------------------- */ + /* Document-Instance-Methods */ + /* -------------------------------------------------------------------------- */ + + expectType>(AutoTypeModelInstance.instanceFn()); + + + return AutoTypeModel; +} diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 09e36e6cc00..dd1855bb6bc 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 } from 'mongoose'; +import { expectError, expectType } from 'tsd'; enum Genre { Action, @@ -305,3 +305,168 @@ function gh11435(): void { new Schema({}, { expires: '5 seconds' }); expectError(new Schema({}, { expireAfterSeconds: '5 seconds' })); new Schema({}, { expireAfterSeconds: 5 }); + +export type M0_0aAutoTypedSchemaType = { + 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' + } +} + +export function m0_0aSchema() { + 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() { return 'Returned from staticFn'; } + }, + methods: { + instanceFn() { return 'Returned from DocumentInstanceFn'; } + } + }); + + type InferredSchemaType = InferSchemaType + + expectType({} as InferredSchemaType); + + expectError({} as InferredSchemaType); + + // 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?: Schema.Types.Mixed; + mixed2?: Schema.Types.Mixed; + mixed3?: Schema.Types.Mixed; + objectId1?: Schema.Types.ObjectId; + objectId2?: Schema.Types.ObjectId; + objectId3?: Schema.Types.ObjectId; + customSchema?:Int8; + map1?: Map; + map2?: Map; + } + + 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 } + }); + + type InferredTestSchemaType = InferSchemaType + + expectType({} as InferredTestSchemaType); + + const SchemaWithCustomTypeKey = new Schema({ + name: { + customKTypeKey: String, + required: true + } + }, { + typeKey: 'customKTypeKey' + }); + + expectType({} as InferSchemaType['name']); + + return AutoTypedSchema; +} diff --git a/types/document.d.ts b/types/document.d.ts index 2e8cea72beb..d889a019ce0 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -170,7 +170,7 @@ declare module 'mongoose' { modelName: string; /** Returns the model with the given name on this document's associated connection. */ - model>(name: string): ModelType; + model>(name: string): ModelType; /** * Overwrite all values in this document with the values of `obj`, except diff --git a/types/index.d.ts b/types/index.d.ts index 6a5d4db9cac..8ac0b20c5a1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -5,6 +5,7 @@ /// /// /// +/// import events = require('events'); import mongodb = require('mongodb'); @@ -129,13 +130,14 @@ declare module 'mongoose' { */ export function isObjectIdOrHexString(v: any): boolean; - export function model(name: string, schema?: Schema | Schema, collection?: string, options?: CompileModelOptions): Model; - export function model( +export function model( name: string, - schema?: Schema, + schema?: TSchema, collection?: string, options?: CompileModelOptions - ): U; +): U extends Model + ? U + : Model, TQueryHelpers, ObtainSchemaGeneric, {}, TSchema>; /** Returns an array of model names created on this instance of Mongoose. */ export function modelNames(): Array; @@ -230,8 +232,8 @@ declare module 'mongoose' { discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId): U; } - type AnyKeys = { [P in keyof T]?: T[P] | any }; interface AnyObject { [k: string]: any } + type FlexibleObject = { [P in keyof (T & Omit)]?: P extends keyof T ? T[P] : any } type Require_id = T extends { _id?: any } ? (T & { _id: T['_id'] }) : (T & { _id: Types.ObjectId }); @@ -251,8 +253,9 @@ declare module 'mongoose' { } export const Model: Model; - interface Model extends NodeJS.EventEmitter, AcceptsDiscriminator { - new & AnyObject>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; + type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { + + new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; aggregate(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback): Aggregate>; aggregate(pipeline: PipelineStage[], cb: Function): Aggregate>; @@ -295,15 +298,11 @@ declare module 'mongoose' { countDocuments(filter: FilterQuery, options?: QueryOptions, 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, TMethodsAndOverrides, TVirtuals>[]>; + create>(docs: Array, callback: Callback, TMethodsAndOverrides, TVirtuals>[]>): void; + create>(doc: T | DocContents): Promise, TMethodsAndOverrides, TVirtuals>>; + create>(...docs: Array): Promise, TMethodsAndOverrides, TVirtuals>[]>; + create>(doc: T | DocContents, callback: Callback, TMethodsAndOverrides, TVirtuals>>): void; /** * Create the collection for this model. By default, if no indexes are specified, @@ -381,12 +380,12 @@ declare module 'mongoose' { 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 & { rawResult: true }): Promise>; + insertMany(docs: Array>, options?: InsertManyOptions): Promise>>; + insertMany(doc: FlexibleObject, options: InsertManyOptions & { rawResult: true }): Promise>; + insertMany(doc: FlexibleObject, options?: InsertManyOptions): Promise[]>; + insertMany(doc: FlexibleObject, options?: InsertManyOptions, callback?: Callback[] | InsertManyResult>): void; + insertMany(docs: Array>, options?: InsertManyOptions, callback?: Callback> | InsertManyResult>): void; /** * Lists the indexes currently defined in MongoDB. This may or may not be @@ -759,14 +758,18 @@ declare module 'mongoose' { export type PostMiddlewareFunction = (this: ThisType, res: ResType, next: CallbackWithoutResultAndOptionalError) => void | Promise; export type ErrorHandlingMiddlewareFunction = (this: ThisType, err: NativeError, res: ResType, next: CallbackWithoutResultAndOptionalError) => void; - class Schema, TInstanceMethods = {}, TQueryHelpers = {}> extends events.EventEmitter { + class Schema, TInstanceMethods = {}, TQueryHelpers = {}, + TPathTypeKey extends TypeKeyBaseType = DefaultTypeKey, + DocType extends ObtainDocumentType = ObtainDocumentType, + TStaticMethods = {}> + 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) @@ -816,7 +819,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; @@ -1434,7 +1437,7 @@ declare module 'mongoose' { /** Searches array items for the first document with a matching _id. */ id(id: any): (T extends Types.Subdocument ? T : Types.Subdocument> & T) | null; - push(...args: (AnyKeys & AnyObject)[]): number; + push(...args: (FlexibleObject & AnyObject)[]): number; } class Map extends global.Map { @@ -1979,7 +1982,7 @@ declare module 'mongoose' { [key: string]: any; }; - type ApplyBasicQueryCasting = T | T[] | any; + type ApplyBasicQueryCasting = T extends any[] ? T[0] & defaultT: defaultT; type Condition = ApplyBasicQueryCasting | QuerySelector>; type _FilterQuery = { @@ -2013,22 +2016,22 @@ declare module 'mongoose' { type _UpdateQuery = { /** @see https://docs.mongodb.com/manual/reference/operator/update-field/ */ - $currentDate?: AnyKeys & AnyObject; - $inc?: AnyKeys & AnyObject; - $min?: AnyKeys & AnyObject; - $max?: AnyKeys & AnyObject; - $mul?: AnyKeys & AnyObject; + $currentDate?: FlexibleObject & AnyObject; + $inc?: FlexibleObject & AnyObject; + $min?: FlexibleObject & AnyObject; + $max?: FlexibleObject & AnyObject; + $mul?: FlexibleObject & AnyObject; $rename?: { [key: string]: string }; - $set?: AnyKeys & AnyObject; - $setOnInsert?: AnyKeys & AnyObject; - $unset?: AnyKeys & AnyObject; + $set?: FlexibleObject & AnyObject; + $setOnInsert?: FlexibleObject & AnyObject; + $unset?: FlexibleObject & AnyObject; /** @see https://docs.mongodb.com/manual/reference/operator/update-array/ */ - $addToSet?: AnyKeys & AnyObject; - $pop?: AnyKeys & AnyObject; - $pull?: AnyKeys & AnyObject; - $push?: AnyKeys & AnyObject; - $pullAll?: AnyKeys & AnyObject; + $addToSet?: FlexibleObject & AnyObject; + $pop?: FlexibleObject & AnyObject; + $pull?: FlexibleObject & AnyObject; + $push?: FlexibleObject & AnyObject; + $pullAll?: FlexibleObject & AnyObject; /** @see https://docs.mongodb.com/manual/reference/operator/update-bitwise/ */ $bit?: { @@ -2048,7 +2051,7 @@ declare module 'mongoose' { [Extract] extends [never] ? T : T | string; type _UpdateQueryDef = { - [K in keyof T]?: __UpdateDefProperty; + [K in keyof T]?: __UpdateDefProperty | any; }; /** diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts new file mode 100644 index 00000000000..6894145e512 --- /dev/null +++ b/types/inferschematype.d.ts @@ -0,0 +1,134 @@ +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 = SchemaType extends Schema + ? IsItRecordAndNotAny extends true ? DocType : ObtainSchemaGeneric + : unknown; + + /** + * @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, TPathTypeKey:TPathTypeKey, DocType: DocType, TStaticMethods: TStaticMethods }[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 = T extends any[] ? false : T extends Record ? true : false; + +/** + * @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 ? K : 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)[] ? ResolvePathType[] : +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 ObjectConstructor | typeof Schema.Types.Mixed ? Schema.Types.Mixed : +keyof PathValueType extends never ? Schema.Types.Mixed : +PathValueType extends MapConstructor ? Map> : +PathValueType extends typeof SchemaType ? PathValueType['prototype'] : +unknown \ No newline at end of file diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 64edcbd2608..b9863ed52cc 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -8,7 +8,11 @@ declare module 'mongoose' { currentTime?: () => (NativeDate | number); } - interface SchemaOptions { + type TypeKeyBaseType = string; + + type DefaultTypeKey = 'type' + + type 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 @@ -137,7 +141,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 @@ -184,6 +188,16 @@ 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?: StaticMethods, + + /** + * Document instance methods. + */ + methods?: InstanceMethods, } } \ No newline at end of file