From b7f70f8883ba5af4a370c4f4500ef74f6a236d05 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sun, 6 Mar 2022 02:51:46 +0100 Subject: [PATCH 01/54] add tsafe package to devDependencies --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 29cd22e9e61..3a7ca5a99a9 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "pug": "3.0.2", "q": "1.5.1", "serve-handler": "6.1.3", + "tsafe": "^0.9.1", "tsd": "0.19.1", "typescript": "4.5.5", "uuid": "8.3.2", @@ -116,4 +117,4 @@ "target": "ES2017" } } -} \ No newline at end of file +} From 8c891e4c7c35d3b52bd54c23dc69e44a44765906 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sun, 6 Mar 2022 02:52:46 +0100 Subject: [PATCH 02/54] create utilities-types helps to infer schema type --- types/infer-doc-type.d.ts | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 types/infer-doc-type.d.ts diff --git a/types/infer-doc-type.d.ts b/types/infer-doc-type.d.ts new file mode 100644 index 00000000000..626e2409302 --- /dev/null +++ b/types/infer-doc-type.d.ts @@ -0,0 +1,41 @@ +import { Schema } from 'mongoose'; + +type RequiredPropertyKeys = { + [K in keyof T]: T[K] extends { required: any } ? K : never; +}[keyof T]; + +type RequiredProperties = { + [K in RequiredPropertyKeys]: T[K]; +}; + +type OptionalPropertyKeys = { + [K in keyof T]: T[K] extends { required: any } ? never : K; +}[keyof T]; + +type OptionalProperties = { + [K in OptionalPropertyKeys]?: T[K]; +}; + +type ResolvePropertyType = PropertyValue extends ( + ...args: any +) => any + ? ReturnType + : PropertyValue; + +export type ObtainDocumentPropertyType = ResolvePropertyType< + PropertyValue extends { type: any } + ? ResolvePropertyType + : PropertyValue +>; + +export type ObtainDocumentType = { + [K in keyof (RequiredProperties & + OptionalProperties)]: ObtainDocumentPropertyType; +}; + +export type InferSchemaType = SchemaType extends Schema< + infer DocType +> + ? ObtainDocumentType + : unknown; + From 3c2fd6bb53a15b0e61b09dd9437cc1e665d6a06a Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sun, 6 Mar 2022 03:15:25 +0100 Subject: [PATCH 03/54] Make some changes that obtain schema type & create some tests --- test/types/models.test.ts | 18 ++++++++++++++++++ test/types/schema.test.ts | 30 +++++++++++++++++++++++++++++- types/index.d.ts | 7 ++++--- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 5fa7c3b1465..07d83c215ed 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 { m0_0aSchema } from './schema.test'; function conventionalSyntax(): void { interface ITest extends Document { @@ -205,4 +206,21 @@ function inheritance() { await this.findOneAndDelete({ installationId }); } } +} + +function m0_0aModel() { + const AutoTypeSchema = m0_0aSchema(); + const AutoTypeModel = model('AutoTypeModel', AutoTypeSchema); + + /* -------------------------------------------------------------------------- */ + /* Model-functions-test */ + /* -------------------------------------------------------------------------- */ + + AutoTypeModel.create({ }); + + /* -------------------------------------------------------------------------- */ + /* Instance-Test */ + /* -------------------------------------------------------------------------- */ + + const AutoTypeModelInstance = new AutoTypeModel({ }); } \ No newline at end of file diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index d55b67e80bb..c56a8e2c897 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1,5 +1,7 @@ import { Schema, Document, SchemaDefinition, Model } from 'mongoose'; -import { expectType, expectNotType, expectError } from 'tsd'; +import { expectError } from 'tsd'; +import { assert, Equals } from 'tsafe'; +import { InferSchemaType } from '../../types/infer-doc-type'; enum Genre { Action, @@ -286,4 +288,30 @@ function gh11448() { const userSchema = new Schema({ name: String, age: Number }); userSchema.pick>(['age']); +} + +export type M0_0aAutoTypedSchemaType = { + userName: string; + description?: string; +} + +export function m0_0aSchema() { + const AutoTypedSchema = new Schema({ + userName: { + type: String, + required: [true, 'userName is required'] + }, + description: String + }); + + assert, M0_0aAutoTypedSchemaType >>(); + + // @ts-expect-error: This should complain because isNotExist property not exist in AutoTypeSchema. + assert, { + userName: string; + description?: string; + isNotExist: boolean; + }>>(); + + return AutoTypedSchema; } \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts index 8ed68eaf039..2b2e6e7069d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -6,6 +6,7 @@ import events = require('events'); import mongodb = require('mongodb'); import mongoose = require('mongoose'); import stream = require('stream'); +import { ObtainDocumentPropertyType, ObtainDocumentType } from './infer-doc-type'; declare module 'mongoose' { @@ -570,7 +571,7 @@ declare module 'mongoose' { discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId): U; } - type AnyKeys = { [P in keyof T]?: T[P] | any }; + type AnyKeys = { [P in keyof T]?: ObtainDocumentPropertyType }; interface AnyObject { [k: string]: any } type Require_id = T extends { _id?: any } ? (T & { _id: T['_id'] }) : (T & { _id: Types.ObjectId }); @@ -592,7 +593,7 @@ declare module 'mongoose' { export const Model: Model; interface Model extends NodeJS.EventEmitter, AcceptsDiscriminator { - new & AnyObject>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; + new & AnyObject>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; aggregate(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback): Aggregate>; aggregate(pipeline: PipelineStage[], cb: Function): Aggregate>; @@ -1101,7 +1102,7 @@ declare module 'mongoose' { /** * 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; From c6b6324b2008f49bcd4efe6c73bcef8d4cb410a5 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 12:34:54 +0100 Subject: [PATCH 04/54] Remove tsafe & rewrite related tests --- package.json | 1 - test/types/models.test.ts | 2 +- test/types/schema.test.ts | 14 +++++--------- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 3a7ca5a99a9..6e0dc9635f9 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "pug": "3.0.2", "q": "1.5.1", "serve-handler": "6.1.3", - "tsafe": "^0.9.1", "tsd": "0.19.1", "typescript": "4.5.5", "uuid": "8.3.2", diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 07d83c215ed..3f480f5cf3d 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -1,6 +1,6 @@ import { Schema, Document, Model, Types, connection, model } from 'mongoose'; import { expectError } from 'tsd'; -import { m0_0aSchema } from './schema.test'; +import { m0_0aSchema } from './schema.test'; function conventionalSyntax(): void { interface ITest extends Document { diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index c56a8e2c897..c143493305c 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1,6 +1,5 @@ import { Schema, Document, SchemaDefinition, Model } from 'mongoose'; -import { expectError } from 'tsd'; -import { assert, Equals } from 'tsafe'; +import { expectError, expectType } from 'tsd'; import { InferSchemaType } from '../../types/infer-doc-type'; enum Genre { @@ -304,14 +303,11 @@ export function m0_0aSchema() { description: String }); - assert, M0_0aAutoTypedSchemaType >>(); + type InferredSchemaType = InferSchemaType - // @ts-expect-error: This should complain because isNotExist property not exist in AutoTypeSchema. - assert, { - userName: string; - description?: string; - isNotExist: boolean; - }>>(); + expectType({} as InferredSchemaType); + + expectError({} as InferredSchemaType); return AutoTypedSchema; } \ No newline at end of file From 9f30615db94a5588b419e05b7351166690112c4f Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 12:44:11 +0100 Subject: [PATCH 05/54] Reimplement auto DocType inference --- test/types/models.test.ts | 2 -- types/index.d.ts | 17 ++++++++--------- types/infer-doc-type.d.ts | 15 +++++++-------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 3f480f5cf3d..2b9f8f131b0 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -216,8 +216,6 @@ function m0_0aModel() { /* Model-functions-test */ /* -------------------------------------------------------------------------- */ - AutoTypeModel.create({ }); - /* -------------------------------------------------------------------------- */ /* Instance-Test */ /* -------------------------------------------------------------------------- */ diff --git a/types/index.d.ts b/types/index.d.ts index 2b2e6e7069d..200d768ae98 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -6,7 +6,7 @@ import events = require('events'); import mongodb = require('mongodb'); import mongoose = require('mongoose'); import stream = require('stream'); -import { ObtainDocumentPropertyType, ObtainDocumentType } from './infer-doc-type'; +import { ObtainDocumentType } from './infer-doc-type'; declare module 'mongoose' { @@ -120,13 +120,12 @@ declare module 'mongoose' { export function isValidObjectId(v: Types.ObjectId): true; export function isValidObjectId(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?: Schema | Schema, collection?: string, options?: CompileModelOptions - ): U; + ): U extends Model ? Model, TQueryHelpers & ITQ, ITM, ITV> & Omit>: Model, TQueryHelpers>; /** Returns an array of model names created on this instance of Mongoose. */ export function modelNames(): Array; @@ -571,7 +570,7 @@ declare module 'mongoose' { discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId): U; } - type AnyKeys = { [P in keyof T]?: ObtainDocumentPropertyType }; + type AnyKeys = { [P in keyof T]?: T[P] | any}; interface AnyObject { [k: string]: any } type Require_id = T extends { _id?: any } ? (T & { _id: T['_id'] }) : (T & { _id: Types.ObjectId }); @@ -593,7 +592,7 @@ declare module 'mongoose' { export const Model: Model; interface Model extends NodeJS.EventEmitter, AcceptsDiscriminator { - new & AnyObject>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; + new & AnyObject>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; aggregate(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback): Aggregate>; aggregate(pipeline: PipelineStage[], cb: Function): Aggregate>; @@ -1098,11 +1097,11 @@ declare module 'mongoose' { type PostMiddlewareFunction = (this: ThisType, res: ResType, next: (err?: CallbackError) => void) => void | Promise; type ErrorHandlingMiddlewareFunction = (this: ThisType, err: NativeError, res: ResType, next: (err?: CallbackError) => void) => void; - class Schema, TInstanceMethods = any, TQueryHelpers = any> extends events.EventEmitter { + class Schema, TInstanceMethods = any, TQueryHelpers = any, DocDefinition = unknown> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); + constructor(definition?: SchemaDefinition> | DocDefinition, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; diff --git a/types/infer-doc-type.d.ts b/types/infer-doc-type.d.ts index 626e2409302..933978efc54 100644 --- a/types/infer-doc-type.d.ts +++ b/types/infer-doc-type.d.ts @@ -28,14 +28,13 @@ export type ObtainDocumentPropertyType = ResolvePropertyType< : PropertyValue >; -export type ObtainDocumentType = { - [K in keyof (RequiredProperties & - OptionalProperties)]: ObtainDocumentPropertyType; +export type ObtainDocumentType = DoesDocTypeExist extends true ? DocType :{ + [K in keyof (RequiredProperties & + OptionalProperties)]: ObtainDocumentPropertyType; }; -export type InferSchemaType = SchemaType extends Schema< - infer DocType -> - ? ObtainDocumentType - : unknown; +export type InferSchemaType = SchemaType extends Schema + ? DoesDocTypeExist extends true ? DocType : ObtainDocumentType + : unknown; +export type DoesDocTypeExist = keyof DocType extends string ? true : false; From d5f11c7afe682eb1a740d0b269384d7297705003 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 13:18:08 +0100 Subject: [PATCH 06/54] Make Model instance show correct type instead of "any" --- test/types/models.test.ts | 10 +++++++--- types/index.d.ts | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 2b9f8f131b0..44309d3d5dd 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -1,6 +1,6 @@ import { Schema, Document, Model, Types, connection, model } from 'mongoose'; -import { expectError } from 'tsd'; -import { m0_0aSchema } from './schema.test'; +import { expectError, expectType } from 'tsd'; +import { M0_0aAutoTypedSchemaType, m0_0aSchema } from './schema.test'; function conventionalSyntax(): void { interface ITest extends Document { @@ -216,9 +216,13 @@ function m0_0aModel() { /* Model-functions-test */ /* -------------------------------------------------------------------------- */ + AutoTypeModel.create({ }); + /* -------------------------------------------------------------------------- */ /* Instance-Test */ /* -------------------------------------------------------------------------- */ - const AutoTypeModelInstance = new AutoTypeModel({ }); + const AutoTypeModelInstance = new AutoTypeModel({}); + + expectType(AutoTypeModelInstance.userName); } \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts index 200d768ae98..a801fbacbbb 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -592,7 +592,7 @@ declare module 'mongoose' { export const Model: Model; interface Model extends NodeJS.EventEmitter, AcceptsDiscriminator { - new & AnyObject>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; + 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>; From 083ce6f161a4aaddaa1461b296e211205be51d03 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 14:33:39 +0100 Subject: [PATCH 07/54] Refact ApplyBasicQueryCasting type to make schema properties types visible --- test/types/models.test.ts | 2 +- types/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 44309d3d5dd..55f9921c286 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -216,7 +216,7 @@ function m0_0aModel() { /* Model-functions-test */ /* -------------------------------------------------------------------------- */ - AutoTypeModel.create({ }); + AutoTypeModel.find({ }); /* -------------------------------------------------------------------------- */ /* Instance-Test */ diff --git a/types/index.d.ts b/types/index.d.ts index a801fbacbbb..40af09a8ecd 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2479,7 +2479,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 = { From f33b24a97a67550fc7557ec4e4ab5527eba9ff60 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 15:46:13 +0100 Subject: [PATCH 08/54] Make schema props type visible --- test/types/models.test.ts | 1 + test/types/schema.test.ts | 16 +++++++++++++++- types/index.d.ts | 20 ++++++++++---------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 55f9921c286..feebcc20cd9 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -217,6 +217,7 @@ function m0_0aModel() { /* -------------------------------------------------------------------------- */ AutoTypeModel.find({ }); + AutoTypeModel.create({}); /* -------------------------------------------------------------------------- */ /* Instance-Test */ diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index c143493305c..955d0bfa099 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -292,6 +292,10 @@ function gh11448() { export type M0_0aAutoTypedSchemaType = { userName: string; description?: string; + nested?: { + age: number; + hobby?: string + } } export function m0_0aSchema() { @@ -300,7 +304,17 @@ export function m0_0aSchema() { type: String, required: [true, 'userName is required'] }, - description: String + description: String, + nested: new Schema({ + age: { + type: Number, + required: true + }, + hobby: { + type: String, + required: false + } + }) }); type InferredSchemaType = InferSchemaType diff --git a/types/index.d.ts b/types/index.d.ts index 40af09a8ecd..98249f2045b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -635,15 +635,15 @@ 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: (T)[], options?: SaveOptions): Promise[]>; + create(docs: (T)[], callback: Callback[]>): void; + create(doc: T): Promise>; + create(doc: T, 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 the collection for this model. By default, if no indexes are specified, @@ -2548,7 +2548,7 @@ declare module 'mongoose' { [Extract] extends [never] ? T : T | string; type _UpdateQueryDef = { - [K in keyof T]?: __UpdateDefProperty; + [K in keyof T]?: __UpdateDefProperty | any; }; /** From c3ae6c56f789f1c957add136bb7aa233c7820e5f Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 15:46:39 +0100 Subject: [PATCH 09/54] Make InferredSchemaType support nested schemas --- types/infer-doc-type.d.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/types/infer-doc-type.d.ts b/types/infer-doc-type.d.ts index 933978efc54..cf617553b67 100644 --- a/types/infer-doc-type.d.ts +++ b/types/infer-doc-type.d.ts @@ -1,7 +1,7 @@ import { Schema } from 'mongoose'; type RequiredPropertyKeys = { - [K in keyof T]: T[K] extends { required: any } ? K : never; + [K in keyof T]: T[K] extends { required: true | [true, string|undefined] } ? K : never; }[keyof T]; type RequiredProperties = { @@ -9,7 +9,7 @@ type RequiredProperties = { }; type OptionalPropertyKeys = { - [K in keyof T]: T[K] extends { required: any } ? never : K; + [K in keyof T]: T[K] extends { required: true | [true, string|undefined] } ? never : K; }[keyof T]; type OptionalProperties = { @@ -22,13 +22,14 @@ type ResolvePropertyType = PropertyValue extends ( ? ReturnType : PropertyValue; -export type ObtainDocumentPropertyType = ResolvePropertyType< +export type ObtainDocumentPropertyType = PropertyValue extends Schema ? InferSchemaType: ResolvePropertyType< PropertyValue extends { type: any } ? ResolvePropertyType : PropertyValue >; -export type ObtainDocumentType = DoesDocTypeExist extends true ? DocType :{ +export type ObtainDocumentType = + DoesDocTypeExist extends true ? DocType : { [K in keyof (RequiredProperties & OptionalProperties)]: ObtainDocumentPropertyType; }; From b7152f794ec5afb8f7953d3153c1b7615dc89cfe Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 17:11:15 +0100 Subject: [PATCH 10/54] Make statics functions auto typed by supplying them to schema options --- lib/schema.js | 2 +- test/model.test.js | 10 +++++++++- test/types/models.test.ts | 11 +++++++++-- test/types/schema.test.ts | 11 ++++++++--- types/index.d.ts | 23 ++++++++++++++--------- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index 06763b7b854..b40a04797b4 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -108,7 +108,7 @@ function Schema(obj, options) { this._indexes = []; this.methods = {}; this.methodOptions = {}; - this.statics = {}; + this.statics = options?.statics || {}; this.tree = {}; this.query = {}; this.childSchemas = []; diff --git a/test/model.test.js b/test/model.test.js index c94b7a48f23..000354a3e37 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -8115,4 +8115,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', function() { + it('should give an array back rather than undefined m0_0aAutoTyped', 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/models.test.ts b/test/types/models.test.ts index feebcc20cd9..49b4ee23c83 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -208,7 +208,7 @@ function inheritance() { } } -function m0_0aModel() { +export function m0_0aModel() { const AutoTypeSchema = m0_0aSchema(); const AutoTypeModel = model('AutoTypeModel', AutoTypeSchema); @@ -219,11 +219,18 @@ function m0_0aModel() { AutoTypeModel.find({ }); AutoTypeModel.create({}); + /* -------------------------------------------------------------------------- */ + /* Model-statics-functions-test */ + /* -------------------------------------------------------------------------- */ + expectType>(AutoTypeModel.staticFn()); + /* -------------------------------------------------------------------------- */ /* Instance-Test */ /* -------------------------------------------------------------------------- */ const AutoTypeModelInstance = new AutoTypeModel({}); - expectType(AutoTypeModelInstance.userName); + expectType(AutoTypeModelInstance.userName); + + return AutoTypeModel; } \ No newline at end of file diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 955d0bfa099..b425ba53bac 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -290,12 +290,17 @@ function gh11448() { } export type M0_0aAutoTypedSchemaType = { + schema: { userName: string; description?: string; nested?: { age: number; hobby?: string } + } + , statics: { + staticFn:()=>'Returned from staticFn' + } } export function m0_0aSchema() { @@ -315,13 +320,13 @@ export function m0_0aSchema() { required: false } }) - }); + }, { statics: { staticFn() {return 'Returned from staticFn';} } }); type InferredSchemaType = InferSchemaType - expectType({} as InferredSchemaType); + expectType({} as InferredSchemaType); - expectError({} as InferredSchemaType); + expectError({} as InferredSchemaType); return AutoTypedSchema; } \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts index 98249f2045b..21cc78b0ba1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -120,12 +120,12 @@ declare module 'mongoose' { export function isValidObjectId(v: Types.ObjectId): true; export function isValidObjectId(v: any): boolean; - export function model( + export function model( name: string, - schema?: Schema | Schema, + schema?: Schema | Schema, collection?: string, options?: CompileModelOptions - ): U extends Model ? Model, TQueryHelpers & ITQ, ITM, ITV> & Omit>: Model, TQueryHelpers>; + ): U extends Model ? Model, TQueryHelpers & ITQ, ITM, ITV, StaticsMethods> & Omit> : Model, TQueryHelpers, {}, {}, StaticsMethods>; /** Returns an array of model names created on this instance of Mongoose. */ export function modelNames(): Array; @@ -591,8 +591,8 @@ declare module 'mongoose' { } export const Model: Model; - interface Model extends NodeJS.EventEmitter, AcceptsDiscriminator { - new(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; + type Model = NodeJS.EventEmitter& AcceptsDiscriminator & StaticsMethods &{ + 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>; @@ -1097,11 +1097,11 @@ declare module 'mongoose' { type PostMiddlewareFunction = (this: ThisType, res: ResType, next: (err?: CallbackError) => void) => void | Promise; type ErrorHandlingMiddlewareFunction = (this: ThisType, err: NativeError, res: ResType, next: (err?: CallbackError) => void) => void; - class Schema, TInstanceMethods = any, TQueryHelpers = any, DocDefinition = unknown> extends events.EventEmitter { + class Schema, TInstanceMethods = any, TQueryHelpers = any, DocDefinition = unknown, StaticsMethods = {}> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocDefinition, options?: SchemaOptions); + constructor(definition?: SchemaDefinition> | DocDefinition, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; @@ -1266,7 +1266,7 @@ declare module 'mongoose' { ? { [path: string]: SchemaDefinitionProperty; } : { [path in keyof T]?: SchemaDefinitionProperty; }; - interface SchemaOptions { + 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 @@ -1424,7 +1424,12 @@ 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?: StaticsMethods, } interface SchemaTimestampsConfig { From d5cbee3c4c859c3a46899d8f245f9bfe36bf22b4 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Wed, 9 Mar 2022 14:39:05 +0100 Subject: [PATCH 11/54] Refact Schema & Model types --- types/index.d.ts | 16 ++++++++++------ types/infer-doc-type.d.ts | 10 ++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index b002e34a33e..d12ce4c71a7 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -7,7 +7,7 @@ import events = require('events'); import mongodb = require('mongodb'); import mongoose = require('mongoose'); import stream = require('stream'); -import { ObtainDocumentType } from './infer-doc-type'; +import { InferSchemaType, ObtainDocumentType, ObtainSchemaGeneric } from './infer-doc-type'; declare module 'mongoose' { @@ -128,12 +128,14 @@ declare module 'mongoose' { */ export function isObjectIdOrHexString(v: any): boolean; - export function model( +export function model( name: string, - schema?: Schema | Schema, + schema?: TSchema, collection?: string, options?: CompileModelOptions - ): U extends Model ? Model, TQueryHelpers & ITQ, ITM, ITV, StaticsMethods> & Omit> : Model, TQueryHelpers, {}, {}, StaticsMethods>; +): U extends Model + ? U + : Model, TQueryHelpers, {}, {}, TSchema>; /** Returns an array of model names created on this instance of Mongoose. */ export function modelNames(): Array; @@ -599,7 +601,9 @@ declare module 'mongoose' { } export const Model: Model; - type Model = NodeJS.EventEmitter& AcceptsDiscriminator & StaticsMethods &{ + type Model = NodeJS.EventEmitter& AcceptsDiscriminator & ObtainSchemaGeneric &{ + + // type Model = NodeJS.EventEmitter& AcceptsDiscriminator & StaticsMethods &{ new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; aggregate(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback): Aggregate>; @@ -1105,7 +1109,7 @@ declare module 'mongoose' { type PostMiddlewareFunction = (this: ThisType, res: ResType, next: (err?: CallbackError) => void) => void | Promise; type ErrorHandlingMiddlewareFunction = (this: ThisType, err: NativeError, res: ResType, next: (err?: CallbackError) => void) => void; - class Schema, TInstanceMethods = any, TQueryHelpers = any, DocDefinition = unknown, StaticsMethods = {}> extends events.EventEmitter { + class Schema, TInstanceMethods = any, TQueryHelpers = any, DocDefinition extends ObtainDocumentType = any, StaticsMethods = {}> extends events.EventEmitter { /** * Create a new schema */ diff --git a/types/infer-doc-type.d.ts b/types/infer-doc-type.d.ts index cf617553b67..8e83e04c0da 100644 --- a/types/infer-doc-type.d.ts +++ b/types/infer-doc-type.d.ts @@ -22,11 +22,13 @@ type ResolvePropertyType = PropertyValue extends ( ? ReturnType : PropertyValue; -export type ObtainDocumentPropertyType = PropertyValue extends Schema ? InferSchemaType: ResolvePropertyType< +type ObtainDocumentPropertyType = PropertyValue extends Schema ? InferSchemaType: ResolvePropertyType< PropertyValue extends { type: any } ? ResolvePropertyType : PropertyValue ->; + >; + +type DoesDocTypeExist = keyof DocType extends string ? true : false; export type ObtainDocumentType = DoesDocTypeExist extends true ? DocType : { @@ -35,7 +37,7 @@ export type ObtainDocumentType = }; export type InferSchemaType = SchemaType extends Schema - ? DoesDocTypeExist extends true ? DocType : ObtainDocumentType + ? DoesDocTypeExist extends true ? DocType : DocDefinition : unknown; -export type DoesDocTypeExist = keyof DocType extends string ? true : false; +export type ObtainSchemaGeneric = TSchema extends Schema ? {DocType:DocType, M:M, TInstanceMethods:TInstanceMethods, TQueryHelpers:TQueryHelpers, DocDefinition:DocDefinition, StaticsMethods:StaticsMethods}[name] : never; \ No newline at end of file From 11b6bedde07f69dcf24e1441fa65068ceaa449a8 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Wed, 9 Mar 2022 17:28:11 +0100 Subject: [PATCH 12/54] Refact some infer-doc-type import lines --- lib/schema.js | 2 +- test/model.test.js | 4 ++-- test/types/schema.test.ts | 3 +-- types/index.d.ts | 2 +- types/infer-doc-type.d.ts | 43 --------------------------------- types/inferschematype.d.ts | 49 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 54 insertions(+), 49 deletions(-) delete mode 100644 types/infer-doc-type.d.ts create mode 100644 types/inferschematype.d.ts diff --git a/lib/schema.js b/lib/schema.js index b40a04797b4..2bdf2e5973e 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -108,7 +108,7 @@ function Schema(obj, options) { this._indexes = []; this.methods = {}; this.methodOptions = {}; - this.statics = options?.statics || {}; + this.statics = (options && options.statics) || {}; this.tree = {}; this.query = {}; this.childSchemas = []; diff --git a/test/model.test.js b/test/model.test.js index 000354a3e37..99895200327 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -8117,8 +8117,8 @@ function pick(obj, keys) { return newObj; } -describe('Check if statics functions that is supplied in schema option is availabe', function() { - it('should give an array back rather than undefined m0_0aAutoTyped', function M0_0aModelJS() { +describe('Check if statics functions that is supplied in schema option is availabe (m0_0a)', function() { + it('should give an 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'); diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index b425ba53bac..bdcf5ae0847 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1,6 +1,5 @@ -import { Schema, Document, SchemaDefinition, Model } from 'mongoose'; +import { Schema, Document, SchemaDefinition, Model, InferSchemaType } from 'mongoose'; import { expectError, expectType } from 'tsd'; -import { InferSchemaType } from '../../types/infer-doc-type'; enum Genre { Action, diff --git a/types/index.d.ts b/types/index.d.ts index d12ce4c71a7..d6b35313ca8 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2,12 +2,12 @@ /// /// /// +/// import events = require('events'); import mongodb = require('mongodb'); import mongoose = require('mongoose'); import stream = require('stream'); -import { InferSchemaType, ObtainDocumentType, ObtainSchemaGeneric } from './infer-doc-type'; declare module 'mongoose' { diff --git a/types/infer-doc-type.d.ts b/types/infer-doc-type.d.ts deleted file mode 100644 index 8e83e04c0da..00000000000 --- a/types/infer-doc-type.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Schema } from 'mongoose'; - -type RequiredPropertyKeys = { - [K in keyof T]: T[K] extends { required: true | [true, string|undefined] } ? K : never; -}[keyof T]; - -type RequiredProperties = { - [K in RequiredPropertyKeys]: T[K]; -}; - -type OptionalPropertyKeys = { - [K in keyof T]: T[K] extends { required: true | [true, string|undefined] } ? never : K; -}[keyof T]; - -type OptionalProperties = { - [K in OptionalPropertyKeys]?: T[K]; -}; - -type ResolvePropertyType = PropertyValue extends ( - ...args: any -) => any - ? ReturnType - : PropertyValue; - -type ObtainDocumentPropertyType = PropertyValue extends Schema ? InferSchemaType: ResolvePropertyType< - PropertyValue extends { type: any } - ? ResolvePropertyType - : PropertyValue - >; - -type DoesDocTypeExist = keyof DocType extends string ? true : false; - -export type ObtainDocumentType = - DoesDocTypeExist extends true ? DocType : { - [K in keyof (RequiredProperties & - OptionalProperties)]: ObtainDocumentPropertyType; -}; - -export type InferSchemaType = SchemaType extends Schema - ? DoesDocTypeExist extends true ? DocType : DocDefinition - : unknown; - -export type ObtainSchemaGeneric = TSchema extends Schema ? {DocType:DocType, M:M, TInstanceMethods:TInstanceMethods, TQueryHelpers:TQueryHelpers, DocDefinition:DocDefinition, StaticsMethods:StaticsMethods}[name] : never; \ No newline at end of file diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts new file mode 100644 index 00000000000..0212af8a369 --- /dev/null +++ b/types/inferschematype.d.ts @@ -0,0 +1,49 @@ +import { Schema, InferSchemaType } from 'mongoose'; + +declare module 'mongoose' { + type ObtainDocumentType = + DoesDocTypeExist extends true ? DocType : { + [K in keyof (RequiredProperties & + OptionalProperties)]: ObtainDocumentPropertyType; + }; + + type InferSchemaType = SchemaType extends Schema + ? DoesDocTypeExist extends true ? DocType : DocDefinition + : unknown; + + type ObtainSchemaGeneric = + TSchema extends Schema + ? { DocType: DocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, DocDefinition: DocDefinition, StaticsMethods: StaticsMethods }[name] + : never; +} + +type RequiredPropertyKeys = { + [K in keyof T]: T[K] extends { required: true | [true, string | undefined] } ? K : never; +}[keyof T]; + +type RequiredProperties = { + [K in RequiredPropertyKeys]: T[K]; +}; + +type OptionalPropertyKeys = { + [K in keyof T]: T[K] extends { required: true | [true, string | undefined] } ? never : K; +}[keyof T]; + +type OptionalProperties = { + [K in OptionalPropertyKeys]?: T[K]; +}; + +type ResolvePropertyType = PropertyValue extends ( + ...args: any +) => any + ? ReturnType + : PropertyValue; + +type ObtainDocumentPropertyType = PropertyValue extends Schema + ? InferSchemaType + : ResolvePropertyType + : PropertyValue +>; + +type DoesDocTypeExist = keyof DocType extends string ? true : false; \ No newline at end of file From 60207715f463a2e9e22bc118d04ec3a995dd93a0 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Thu, 10 Mar 2022 16:50:47 +0100 Subject: [PATCH 13/54] Create some tests for Model functions --- test/types/models.test.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 49b4ee23c83..768beae8c02 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -208,20 +208,30 @@ function inheritance() { } } -export function m0_0aModel() { +export async function m0_0aModel() { const AutoTypeSchema = m0_0aSchema(); const AutoTypeModel = model('AutoTypeModel', AutoTypeSchema); /* -------------------------------------------------------------------------- */ /* Model-functions-test */ /* -------------------------------------------------------------------------- */ + // TODO: check if these tests work after merging this PR: https://github.com/Automattic/mongoose/pull/11503 - AutoTypeModel.find({ }); - AutoTypeModel.create({}); + /* + const testDoc1 = await AutoTypeModel.create({ userName: 'M0_0a' }); + expectType(testDoc.userName); + expectType(testDoc.nested.age); + + const testDoc2 = await AutoTypeModel.findOne({ userName: 'M0_0a' }); + expectType(testDoc2.userName); + expectType(testDoc2.nested.age); + + */ /* -------------------------------------------------------------------------- */ /* Model-statics-functions-test */ /* -------------------------------------------------------------------------- */ + expectType>(AutoTypeModel.staticFn()); /* -------------------------------------------------------------------------- */ From d5059d5c7ece0d1993dffe1f89b8da34cf0347db Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Thu, 10 Mar 2022 17:37:51 +0100 Subject: [PATCH 14/54] Infer document instance methods from methods object supplied in schema options --- lib/schema.js | 2 +- test/document.test.js | 8 ++++++++ test/types/models.test.ts | 7 +++++++ test/types/schema.test.ts | 12 +++++++++++- types/index.d.ts | 16 +++++++++------- types/schemaoptions.d.ts | 9 +++++++-- 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index 2bdf2e5973e..19344364f31 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -106,7 +106,7 @@ function Schema(obj, options) { this.inherits = {}; this.callQueue = []; this._indexes = []; - this.methods = {}; + this.methods = (options && options.methods) || {}; this.methodOptions = {}; this.statics = (options && options.statics) || {}; this.tree = {}; diff --git a/test/document.test.js b/test/document.test.js index 5960b3beb9f..d6e7c26c5e5 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -11122,4 +11122,12 @@ describe('document', function() { assert.ok(!reloaded.nested.foo.$__isNested); assert.strictEqual(reloaded.nested.foo.bar, 66); }); + describe('Check if document instance functions that is supplied in schema option is availabe (m0_0a)', function() { + it('should give an instance function back rather than undefined', 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/types/models.test.ts b/test/types/models.test.ts index 768beae8c02..330f825c37a 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -242,5 +242,12 @@ export async function m0_0aModel() { expectType(AutoTypeModelInstance.userName); + /* -------------------------------------------------------------------------- */ + /* Document-Instance-Methods */ + /* -------------------------------------------------------------------------- */ + + expectType>(AutoTypeModelInstance.instanceFn()); + + return AutoTypeModel; } \ No newline at end of file diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index bdcf5ae0847..f2dd5867ce2 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -299,6 +299,9 @@ export type M0_0aAutoTypedSchemaType = { } , statics: { staticFn:()=>'Returned from staticFn' + }, + methods: { + instanceFn : ()=> 'Returned from DocumentInstanceFn' } } @@ -319,7 +322,14 @@ export function m0_0aSchema() { required: false } }) - }, { statics: { staticFn() {return 'Returned from staticFn';} } }); + }, { + statics: { + staticFn() { return 'Returned from staticFn'; } + }, + methods: { + instanceFn() { return 'Returned from DocumentInstanceFn'; } + } + }); type InferredSchemaType = InferSchemaType diff --git a/types/index.d.ts b/types/index.d.ts index d6b35313ca8..9c4cceba76d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -135,7 +135,7 @@ export function model( options?: CompileModelOptions ): U extends Model ? U - : Model, TQueryHelpers, {}, {}, TSchema>; + : Model, TQueryHelpers, ObtainSchemaGeneric, {}, TSchema>; /** Returns an array of model names created on this instance of Mongoose. */ export function modelNames(): Array; @@ -601,10 +601,9 @@ export function model( } export const Model: Model; - type Model = NodeJS.EventEmitter& AcceptsDiscriminator & ObtainSchemaGeneric &{ + type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { - // type Model = NodeJS.EventEmitter& AcceptsDiscriminator & StaticsMethods &{ - new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; + new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument & */ TMethodsAndOverrides, TVirtuals>; aggregate(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback): Aggregate>; aggregate(pipeline: PipelineStage[], cb: Function): Aggregate>; @@ -1109,11 +1108,14 @@ export function model( type PostMiddlewareFunction = (this: ThisType, res: ResType, next: (err?: CallbackError) => void) => void | Promise; type ErrorHandlingMiddlewareFunction = (this: ThisType, err: NativeError, res: ResType, next: (err?: CallbackError) => void) => void; - class Schema, TInstanceMethods = any, TQueryHelpers = any, DocDefinition extends ObtainDocumentType = any, StaticsMethods = {}> extends events.EventEmitter { + class Schema, TInstanceMethods = {}, TQueryHelpers = any, + DocDefinition extends ObtainDocumentType = any, + StaticsMethods = {}> + extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocDefinition, options?: SchemaOptions); + constructor(definition?: SchemaDefinition> | DocDefinition, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; @@ -1163,7 +1165,7 @@ export function model( method(obj: Partial): this; /** Object of currently defined methods on this schema. */ - methods: { [F in keyof TInstanceMethods]: TInstanceMethods[F] }; + methods: any; /** The original object passed to the schema constructor */ obj: SchemaDefinition>; diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 68875443029..5e8ecb16459 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -8,7 +8,7 @@ declare module 'mongoose' { currentTime?: () => (NativeDate | number); } - interface SchemaOptions { + 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 @@ -183,6 +183,11 @@ declare module 'mongoose' { /** * Model Statics methods. */ - statics?: StaticsMethods, + statics?: StaticsMethods, + + /** + * Document instance methods. + */ + methods?: InstanceMethods, } } \ No newline at end of file From 8fe42628a8dd04dbc9c8594691d6f8fc736fa034 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Thu, 10 Mar 2022 19:10:37 +0100 Subject: [PATCH 15/54] Use auto infered docType as default type every where --- test/types/middleware.test.ts | 6 +++--- types/index.d.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/types/middleware.test.ts b/test/types/middleware.test.ts index 33d013943fe..9987c189b0f 100644 --- a/test/types/middleware.test.ts +++ b/test/types/middleware.test.ts @@ -1,11 +1,11 @@ import { Schema, model, Model, Document, SaveOptions, Query, Aggregate, HydratedDocument } from 'mongoose'; -import { expectError, expectType } from 'tsd'; +import { expectAssignable, expectError, expectType } from 'tsd'; interface ITest extends Document { name?: string; } -const schema: Schema = new Schema({ name: { type: 'String' } }); +const schema: Schema = new Schema({ name: { type: String } }); schema.pre>('find', async function() { console.log('Find', this.getFilter()); @@ -72,6 +72,6 @@ const Test = model('Test', schema); function gh11257(): void { schema.pre('save', { document: true }, function() { - expectType>(this); + expectAssignable>(this); }); } \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts index 9c4cceba76d..d9bf5981005 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -603,7 +603,7 @@ export function model( export const Model: Model; type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { - new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument & */ TMethodsAndOverrides, TVirtuals>; + 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>; @@ -1108,17 +1108,17 @@ export function model( type PostMiddlewareFunction = (this: ThisType, res: ResType, next: (err?: CallbackError) => void) => void | Promise; type ErrorHandlingMiddlewareFunction = (this: ThisType, err: NativeError, res: ResType, next: (err?: CallbackError) => void) => void; - class Schema, TInstanceMethods = {}, TQueryHelpers = any, - DocDefinition extends ObtainDocumentType = any, + class Schema, TInstanceMethods = {}, TQueryHelpers = any, + DocType extends ObtainDocumentType = any, StaticsMethods = {}> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocDefinition, 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) @@ -1168,7 +1168,7 @@ export function model( methods: any; /** The original object passed to the schema constructor */ - obj: SchemaDefinition>; + obj: SchemaDefinition>; /** Gets/sets schema paths. */ path(path: string): ResultType; From 9f4b974869c8fffbe3802490b728237085f0fc8b Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Fri, 11 Mar 2022 16:15:16 +0100 Subject: [PATCH 16/54] Improve ResolvePathType ability to resolve correct type of types string aliases like "String" | "Number" --- test/types/schema.test.ts | 90 +++++++++++++++++++++++++++++++++++++- types/inferschematype.d.ts | 62 +++++++++++++++----------- 2 files changed, 126 insertions(+), 26 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index f2dd5867ce2..1b57203e92e 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1,4 +1,4 @@ -import { Schema, Document, SchemaDefinition, Model, InferSchemaType } from 'mongoose'; +import { Schema, Document, SchemaDefinition, Model, InferSchemaType, SchemaType } from 'mongoose'; import { expectError, expectType } from 'tsd'; enum Genre { @@ -337,5 +337,93 @@ export function m0_0aSchema() { 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); + return AutoTypedSchema; } \ No newline at end of file diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 0212af8a369..e3d244f5652 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -1,10 +1,10 @@ -import { Schema, InferSchemaType } from 'mongoose'; +import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions } from 'mongoose'; declare module 'mongoose' { type ObtainDocumentType = DoesDocTypeExist extends true ? DocType : { - [K in keyof (RequiredProperties & - OptionalProperties)]: ObtainDocumentPropertyType; + [K in keyof (RequiredPaths & + OptionalPaths)]: ObtainDocumentPathType; }; type InferSchemaType = SchemaType extends Schema @@ -17,33 +17,45 @@ declare module 'mongoose' { : never; } -type RequiredPropertyKeys = { - [K in keyof T]: T[K] extends { required: true | [true, string | undefined] } ? K : never; -}[keyof T]; +type DoesDocTypeExist = keyof DocType extends string ? true : false; -type RequiredProperties = { - [K in RequiredPropertyKeys]: T[K]; -}; +type RequiredPathBaseType = { required: true | [true, string | undefined] } + +type PathWithTypePropertyBaseType = { type: any } -type OptionalPropertyKeys = { - [K in keyof T]: T[K] extends { required: true | [true, string | undefined] } ? never : K; +type RequiredPathKeys = { + [K in keyof T]: T[K] extends RequiredPathBaseType ? K : never; }[keyof T]; -type OptionalProperties = { - [K in OptionalPropertyKeys]?: T[K]; +type RequiredPaths = { + [K in RequiredPathKeys]: T[K]; }; -type ResolvePropertyType = PropertyValue extends ( - ...args: any -) => any - ? ReturnType - : PropertyValue; +type OptionalPathKeys = { + [K in keyof T]: T[K] extends RequiredPathBaseType ? never : K; +}[keyof T]; -type ObtainDocumentPropertyType = PropertyValue extends Schema - ? InferSchemaType - : ResolvePropertyType - : PropertyValue ->; +type OptionalPaths = { + [K in OptionalPathKeys]?: T[K]; +}; -type DoesDocTypeExist = keyof DocType extends string ? true : false; \ No newline at end of file +type ObtainDocumentPathType = PathValueType extends Schema + ? InferSchemaType + : ResolvePathType< + PathValueType extends PathWithTypePropertyBaseType ? PathValueType['type'] : PathValueType, + PathValueType extends PathWithTypePropertyBaseType ? Omit : {} + >; + +type ResolvePathType = {}> = +PathValueType extends (infer Item)[] ? ResolvePathType[] : +PathValueType extends StringConstructor | 'string' | 'String' | typeof Schema.Types.String ? string : +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'] : +PathValueType \ No newline at end of file From 4a5d3fa9dec0316e4ed50b27a5e328ec9864e15d Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sat, 12 Mar 2022 02:26:38 +0100 Subject: [PATCH 17/54] create & implement FlexibleObject & refact typos --- test/document.test.js | 2 +- test/model.test.js | 2 +- test/types/models.test.ts | 5 +++-- types/index.d.ts | 23 ++++++++++------------- types/inferschematype.d.ts | 11 ++++++----- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/test/document.test.js b/test/document.test.js index d6e7c26c5e5..0e35cc9c607 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -11122,7 +11122,7 @@ describe('document', function() { assert.ok(!reloaded.nested.foo.$__isNested); assert.strictEqual(reloaded.nested.foo.bar, 66); }); - describe('Check if document instance functions that is supplied in schema option is availabe (m0_0a)', function() { + describe('Check if a document instance function that is supplied in schema option is availabe (m0_0a)', function() { it('should give an instance function back rather than undefined', function M0_0aModelJS() { const testSchema = new mongoose.Schema({}, { methods: { instanceFn() { return 'Returned from DocumentInstanceFn'; } } }); const TestModel = mongoose.model('TestModel', testSchema); diff --git a/test/model.test.js b/test/model.test.js index 99895200327..d9f2f383790 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -8118,7 +8118,7 @@ function pick(obj, keys) { } describe('Check if statics functions that is supplied in schema option is availabe (m0_0a)', function() { - it('should give an static function back rather than undefined', function M0_0aModelJS() { + 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'); diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 330f825c37a..345197fff75 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -215,8 +215,10 @@ export async function m0_0aModel() { /* -------------------------------------------------------------------------- */ /* Model-functions-test */ /* -------------------------------------------------------------------------- */ - // TODO: check if these tests work after merging this PR: https://github.com/Automattic/mongoose/pull/11503 + // Create should works with arbitrary objects. + const testDoc1 = await AutoTypeModel.create({ unExistKey: 'unExistKey' }); + // TODO: check if these tests work after merging this PR: https://github.com/Automattic/mongoose/pull/11503 /* const testDoc1 = await AutoTypeModel.create({ userName: 'M0_0a' }); expectType(testDoc.userName); @@ -225,7 +227,6 @@ export async function m0_0aModel() { const testDoc2 = await AutoTypeModel.findOne({ userName: 'M0_0a' }); expectType(testDoc2.userName); expectType(testDoc2.nested.age); - */ /* -------------------------------------------------------------------------- */ diff --git a/types/index.d.ts b/types/index.d.ts index d9bf5981005..5edcca94108 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -580,8 +580,9 @@ export function model( discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId): U; } - type AnyKeys = { [P in keyof T]?: T[P] | any}; + type AnyKeys = { [P in keyof T]?: 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 }); @@ -601,7 +602,7 @@ export function model( } export const Model: Model; - type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { + type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; @@ -646,15 +647,11 @@ export function model( countDocuments(filter: FilterQuery, options?: QueryOptions, callback?: Callback): QueryWithHelpers, TQueryHelpers, T>; /** Creates a new document or documents */ - create(docs: (T)[], options?: SaveOptions): Promise[]>; - create(docs: (T)[], callback: Callback[]>): void; - create(doc: T): Promise>; - create(doc: T, 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: 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 the collection for this model. By default, if no indexes are specified, @@ -1110,12 +1107,12 @@ export function model( class Schema, TInstanceMethods = {}, TQueryHelpers = any, DocType extends ObtainDocumentType = any, - StaticsMethods = {}> + StaticMethods = {}> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); + constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index e3d244f5652..88ad764fc3d 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -1,8 +1,9 @@ import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions } from 'mongoose'; declare module 'mongoose' { - type ObtainDocumentType = - DoesDocTypeExist extends true ? DocType : { + + type ObtainDocumentType = + DoesDocTypeExist extends true ? EnforcedDocType : { [K in keyof (RequiredPaths & OptionalPaths)]: ObtainDocumentPathType; }; @@ -11,9 +12,9 @@ declare module 'mongoose' { ? DoesDocTypeExist extends true ? DocType : DocDefinition : unknown; - type ObtainSchemaGeneric = - TSchema extends Schema - ? { DocType: DocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, DocDefinition: DocDefinition, StaticsMethods: StaticsMethods }[name] + type ObtainSchemaGeneric = + TSchema extends Schema + ? { DocType: DocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, DocDefinition: DocDefinition, StaticMethods: StaticMethods }[name] : never; } From a57b871701d0707b6ddaf313c9fe5bf40ab83e01 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sat, 12 Mar 2022 02:28:52 +0100 Subject: [PATCH 18/54] create & implement FlexibleObject & refact typos --- test/document.test.js | 2 +- test/model.test.js | 2 +- test/types/models.test.ts | 5 +++-- types/index.d.ts | 23 ++++++++++------------- types/inferschematype.d.ts | 11 ++++++----- types/schemaoptions.d.ts | 4 ++-- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/test/document.test.js b/test/document.test.js index d6e7c26c5e5..0e35cc9c607 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -11122,7 +11122,7 @@ describe('document', function() { assert.ok(!reloaded.nested.foo.$__isNested); assert.strictEqual(reloaded.nested.foo.bar, 66); }); - describe('Check if document instance functions that is supplied in schema option is availabe (m0_0a)', function() { + describe('Check if a document instance function that is supplied in schema option is availabe (m0_0a)', function() { it('should give an instance function back rather than undefined', function M0_0aModelJS() { const testSchema = new mongoose.Schema({}, { methods: { instanceFn() { return 'Returned from DocumentInstanceFn'; } } }); const TestModel = mongoose.model('TestModel', testSchema); diff --git a/test/model.test.js b/test/model.test.js index 99895200327..d9f2f383790 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -8118,7 +8118,7 @@ function pick(obj, keys) { } describe('Check if statics functions that is supplied in schema option is availabe (m0_0a)', function() { - it('should give an static function back rather than undefined', function M0_0aModelJS() { + 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'); diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 330f825c37a..345197fff75 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -215,8 +215,10 @@ export async function m0_0aModel() { /* -------------------------------------------------------------------------- */ /* Model-functions-test */ /* -------------------------------------------------------------------------- */ - // TODO: check if these tests work after merging this PR: https://github.com/Automattic/mongoose/pull/11503 + // Create should works with arbitrary objects. + const testDoc1 = await AutoTypeModel.create({ unExistKey: 'unExistKey' }); + // TODO: check if these tests work after merging this PR: https://github.com/Automattic/mongoose/pull/11503 /* const testDoc1 = await AutoTypeModel.create({ userName: 'M0_0a' }); expectType(testDoc.userName); @@ -225,7 +227,6 @@ export async function m0_0aModel() { const testDoc2 = await AutoTypeModel.findOne({ userName: 'M0_0a' }); expectType(testDoc2.userName); expectType(testDoc2.nested.age); - */ /* -------------------------------------------------------------------------- */ diff --git a/types/index.d.ts b/types/index.d.ts index d9bf5981005..5edcca94108 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -580,8 +580,9 @@ export function model( discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId): U; } - type AnyKeys = { [P in keyof T]?: T[P] | any}; + type AnyKeys = { [P in keyof T]?: 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 }); @@ -601,7 +602,7 @@ export function model( } export const Model: Model; - type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { + type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; @@ -646,15 +647,11 @@ export function model( countDocuments(filter: FilterQuery, options?: QueryOptions, callback?: Callback): QueryWithHelpers, TQueryHelpers, T>; /** Creates a new document or documents */ - create(docs: (T)[], options?: SaveOptions): Promise[]>; - create(docs: (T)[], callback: Callback[]>): void; - create(doc: T): Promise>; - create(doc: T, 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: 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 the collection for this model. By default, if no indexes are specified, @@ -1110,12 +1107,12 @@ export function model( class Schema, TInstanceMethods = {}, TQueryHelpers = any, DocType extends ObtainDocumentType = any, - StaticsMethods = {}> + StaticMethods = {}> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); + constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index e3d244f5652..88ad764fc3d 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -1,8 +1,9 @@ import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions } from 'mongoose'; declare module 'mongoose' { - type ObtainDocumentType = - DoesDocTypeExist extends true ? DocType : { + + type ObtainDocumentType = + DoesDocTypeExist extends true ? EnforcedDocType : { [K in keyof (RequiredPaths & OptionalPaths)]: ObtainDocumentPathType; }; @@ -11,9 +12,9 @@ declare module 'mongoose' { ? DoesDocTypeExist extends true ? DocType : DocDefinition : unknown; - type ObtainSchemaGeneric = - TSchema extends Schema - ? { DocType: DocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, DocDefinition: DocDefinition, StaticsMethods: StaticsMethods }[name] + type ObtainSchemaGeneric = + TSchema extends Schema + ? { DocType: DocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, DocDefinition: DocDefinition, StaticMethods: StaticMethods }[name] : never; } diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 5e8ecb16459..f426446aa82 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -8,7 +8,7 @@ declare module 'mongoose' { currentTime?: () => (NativeDate | number); } - interface SchemaOptions { + 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 @@ -183,7 +183,7 @@ declare module 'mongoose' { /** * Model Statics methods. */ - statics?: StaticsMethods, + statics?: StaticMethods, /** * Document instance methods. From 3e23caee172164be65cadb9a5657b876aeb02ede Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sat, 12 Mar 2022 17:43:58 +0100 Subject: [PATCH 19/54] Delete AnyKeys type & refact create & insertMany types --- test/types/create.test.ts | 2 +- test/types/models.test.ts | 17 ++++++------- types/index.d.ts | 51 +++++++++++++++++++-------------------- 3 files changed, 34 insertions(+), 36 deletions(-) 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/models.test.ts b/test/types/models.test.ts index 345197fff75..0723da97276 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -1,5 +1,5 @@ import { Schema, Document, Model, Types, connection, model } from 'mongoose'; -import { expectError, expectType } from 'tsd'; +import { expectAssignable, expectError, expectType } from 'tsd'; import { M0_0aAutoTypedSchemaType, m0_0aSchema } from './schema.test'; function conventionalSyntax(): void { @@ -216,18 +216,17 @@ export async function m0_0aModel() { /* Model-functions-test */ /* -------------------------------------------------------------------------- */ // Create should works with arbitrary objects. - const testDoc1 = await AutoTypeModel.create({ unExistKey: 'unExistKey' }); + const randomObject = await AutoTypeModel.create({ unExistKey: 'unExistKey', description: 'st' }); + expectType(randomObject.unExistKey); + expectType(randomObject.userName); - // TODO: check if these tests work after merging this PR: https://github.com/Automattic/mongoose/pull/11503 - /* const testDoc1 = await AutoTypeModel.create({ userName: 'M0_0a' }); - expectType(testDoc.userName); - expectType(testDoc.nested.age); + expectType(testDoc1.userName); + expectType(testDoc1.description); const testDoc2 = await AutoTypeModel.findOne({ userName: 'M0_0a' }); - expectType(testDoc2.userName); - expectType(testDoc2.nested.age); - */ + expectType(testDoc2?.userName); + expectType(testDoc2?.description); /* -------------------------------------------------------------------------- */ /* Model-statics-functions-test */ diff --git a/types/index.d.ts b/types/index.d.ts index 04536a8155e..d487387d5f6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -343,7 +343,6 @@ export function model( discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId): U; } - type AnyKeys = { [P in keyof T]?: any}; interface AnyObject { [k: string]: any } type FlexibleObject = { [P in keyof (T & Omit)]?: P extends keyof T ? T[P] : any } @@ -410,11 +409,11 @@ export function model( countDocuments(filter: FilterQuery, options?: QueryOptions, callback?: Callback): QueryWithHelpers, TQueryHelpers, T>; /** Creates a new document or documents */ - 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, @@ -492,12 +491,12 @@ export function model( 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 @@ -1544,7 +1543,7 @@ export function model( /** 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 { @@ -2123,22 +2122,22 @@ export function model( 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?: { From 15a9213959ea8427cd31745ad2485d2ff89fe3a8 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sun, 13 Mar 2022 14:26:25 +0100 Subject: [PATCH 20/54] Support obtaining enum type --- test/types/models.test.ts | 2 ++ test/types/schema.test.ts | 18 ++++++++++++++++-- types/inferschematype.d.ts | 4 +++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 0723da97276..051fcfc7a77 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -241,6 +241,8 @@ export async function m0_0aModel() { const AutoTypeModelInstance = new AutoTypeModel({}); expectType(AutoTypeModelInstance.userName); + expectType(AutoTypeModelInstance.favoritDrink); + expectType(AutoTypeModelInstance.favoritColorMode); /* -------------------------------------------------------------------------- */ /* Document-Instance-Methods */ diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 1b57203e92e..9543ce92b4e 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -295,7 +295,9 @@ export type M0_0aAutoTypedSchemaType = { nested?: { age: number; hobby?: string - } + }, + favoritDrink?: 'Tea' | 'Coffee', + favoritColorMode:'dark' | 'light' } , statics: { staticFn:()=>'Returned from staticFn' @@ -321,7 +323,19 @@ export function m0_0aSchema() { 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'; } diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 88ad764fc3d..4ce0a127fc8 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -47,9 +47,11 @@ type ObtainDocumentPathType = PathValueType extends Schema PathValueType extends PathWithTypePropertyBaseType ? Omit : {} >; +type PathEnumOrString = T extends (infer E)[] ? E : T extends { values: any } ? PathEnumOrString : string; + type ResolvePathType = {}> = PathValueType extends (infer Item)[] ? ResolvePathType[] : -PathValueType extends StringConstructor | 'string' | 'String' | typeof Schema.Types.String ? string : +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 : From 638a0b6bb1c180b21b39ff9a7d8425a332f86cee Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sun, 13 Mar 2022 16:39:49 +0100 Subject: [PATCH 21/54] Support custom typeKey --- test/types/schema.test.ts | 11 +++++++++++ types/index.d.ts | 11 ++++++----- types/inferschematype.d.ts | 26 +++++++++++++------------- types/schemaoptions.d.ts | 8 ++++++-- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 9543ce92b4e..65c3a525aa5 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -439,5 +439,16 @@ export function m0_0aSchema() { expectType({} as InferredTestSchemaType); + const SchemaWithCustomTypeKey = new Schema({ + name: { + customKTypeKey: String, + required: true + } + }, { + typeKey: 'customKTypeKey' + }); + + expectType({} as InferSchemaType['name']); + return AutoTypedSchema; } \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts index d487387d5f6..ac26dc1f392 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -867,17 +867,18 @@ export function model( 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 = any, - DocType extends ObtainDocumentType = any, + class Schema, TInstanceMethods = {}, TQueryHelpers = any, + PathTypeKey extends TypeKeyBaseType = DefaultTypeKey, + DocType extends ObtainDocumentType = any, StaticMethods = {}> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocType, 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) @@ -927,7 +928,7 @@ export function model( methods: any; /** 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 index 4ce0a127fc8..94eb4f0d9c0 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -1,20 +1,20 @@ -import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions } from 'mongoose'; +import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions, TypeKeyBaseType } from 'mongoose'; declare module 'mongoose' { - type ObtainDocumentType = + type ObtainDocumentType = DoesDocTypeExist extends true ? EnforcedDocType : { [K in keyof (RequiredPaths & - OptionalPaths)]: ObtainDocumentPathType; + OptionalPaths)]: ObtainDocumentPathType; }; - type InferSchemaType = SchemaType extends Schema - ? DoesDocTypeExist extends true ? DocType : DocDefinition + type InferSchemaType = SchemaType extends Schema + ? DoesDocTypeExist extends true ? DocType : ObtainSchemaGeneric : unknown; - type ObtainSchemaGeneric = - TSchema extends Schema - ? { DocType: DocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, DocDefinition: DocDefinition, StaticMethods: StaticMethods }[name] + type ObtainSchemaGeneric = + TSchema extends Schema + ? { EnforcedDocType: EnforcedDocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, PathTypeKey:PathTypeKey, DocType: DocType, StaticMethods: StaticMethods }[name] : never; } @@ -22,7 +22,7 @@ type DoesDocTypeExist = keyof DocType extends string ? true : false; type RequiredPathBaseType = { required: true | [true, string | undefined] } -type PathWithTypePropertyBaseType = { type: any } +type PathWithTypePropertyBaseType = { [k in TypeKey]: any } type RequiredPathKeys = { [K in keyof T]: T[K] extends RequiredPathBaseType ? K : never; @@ -40,11 +40,11 @@ type OptionalPaths = { [K in OptionalPathKeys]?: T[K]; }; -type ObtainDocumentPathType = PathValueType extends Schema +type ObtainDocumentPathType = PathValueType extends Schema ? InferSchemaType : ResolvePathType< - PathValueType extends PathWithTypePropertyBaseType ? PathValueType['type'] : PathValueType, - PathValueType extends PathWithTypePropertyBaseType ? Omit : {} + PathValueType extends PathWithTypePropertyBaseType ? PathValueType[typeKey] : PathValueType, + PathValueType extends PathWithTypePropertyBaseType ? Omit : {} >; type PathEnumOrString = T extends (infer E)[] ? E : T extends { values: any } ? PathEnumOrString : string; @@ -61,4 +61,4 @@ PathValueType extends ObjectConstructor | typeof Schema.Types.Mixed ? Schema.Typ keyof PathValueType extends never ? Schema.Types.Mixed : PathValueType extends MapConstructor ? Map> : PathValueType extends typeof SchemaType ? PathValueType['prototype'] : -PathValueType \ No newline at end of file +unknown \ No newline at end of file diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index f426446aa82..9f64fa9aa26 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 | number | symbol; + + 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 @@ -131,7 +135,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 From c938e1ed612092e90935799fb91872c8e2438fba Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sun, 13 Mar 2022 19:32:24 +0100 Subject: [PATCH 22/54] Refact previous commit --- types/inferschematype.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 94eb4f0d9c0..fc578d453d9 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -2,7 +2,7 @@ import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions, TypeKeyBaseType declare module 'mongoose' { - type ObtainDocumentType = + type ObtainDocumentType = DoesDocTypeExist extends true ? EnforcedDocType : { [K in keyof (RequiredPaths & OptionalPaths)]: ObtainDocumentPathType; From ce0f299b8a209c7da7de226d511c9de873d2410c Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Tue, 22 Mar 2022 22:39:42 +0100 Subject: [PATCH 23/54] Provide JSDoc comments for inferschematype types. --- types/index.d.ts | 10 ++-- types/inferschematype.d.ts | 102 +++++++++++++++++++++++++++++++------ types/schemaoptions.d.ts | 2 +- 3 files changed, 92 insertions(+), 22 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index ac26dc1f392..4908466805f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -364,7 +364,7 @@ export function model( } export const Model: Model; - type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { + type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; @@ -868,14 +868,14 @@ export function model( export type ErrorHandlingMiddlewareFunction = (this: ThisType, err: NativeError, res: ResType, next: CallbackWithoutResultAndOptionalError) => void; class Schema, TInstanceMethods = {}, TQueryHelpers = any, - PathTypeKey extends TypeKeyBaseType = DefaultTypeKey, - DocType extends ObtainDocumentType = any, - StaticMethods = {}> + TPathTypeKey extends TypeKeyBaseType = DefaultTypeKey, + DocType extends ObtainDocumentType = any, + TStaticMethods = {}> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); + constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index fc578d453d9..6894145e512 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -1,54 +1,124 @@ import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions, TypeKeyBaseType } from 'mongoose'; declare module 'mongoose' { - - type ObtainDocumentType = - DoesDocTypeExist extends true ? EnforcedDocType : { + /** + * @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; + 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 - ? DoesDocTypeExist extends true ? DocType : ObtainSchemaGeneric + ? IsItRecordAndNotAny extends true ? DocType : ObtainSchemaGeneric : unknown; - type ObtainSchemaGeneric = - TSchema extends Schema - ? { EnforcedDocType: EnforcedDocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, PathTypeKey:PathTypeKey, DocType: DocType, StaticMethods: StaticMethods }[name] - : never; + /** + * @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; -type DoesDocTypeExist = keyof DocType extends string ? 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] } -type PathWithTypePropertyBaseType = { [k in TypeKey]: any } +/** + * @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]; }; -type ObtainDocumentPathType = PathValueType extends Schema +/** + * @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 : {} + PathValueType extends PathWithTypePropertyBaseType ? PathValueType[TypeKey] : PathValueType, + PathValueType extends PathWithTypePropertyBaseType ? Omit : {} >; -type PathEnumOrString = T extends (infer E)[] ? E : T extends { values: any } ? PathEnumOrString : string; +/** + * @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 : diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 9f64fa9aa26..0adfee71f9f 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -8,7 +8,7 @@ declare module 'mongoose' { currentTime?: () => (NativeDate | number); } - type TypeKeyBaseType = string | number | symbol; + type TypeKeyBaseType = string; type DefaultTypeKey = 'type' From bc62a7741985294d2e11485544531286c3670db2 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Wed, 23 Mar 2022 01:03:51 +0100 Subject: [PATCH 24/54] Merge branch 'master' of https://github.com/Automattic/mongoose --- .github/.dependabot.yml | 10 + .github/workflows/test.yml | 2 +- CHANGELOG.md | 9 + benchmarks/index.js | 1 - benchmarks/validate.js | 21 +- docs/images/helloclub.svg | 37 ++- docs/loadSponsorData.js | 45 +++ docs/middleware.md | 2 +- docs/migrating_to_6.md | 26 ++ examples/statics/statics.js | 2 +- index.pug | 244 +--------------- lib/browserDocument.js | 1 + lib/connection.js | 8 +- lib/cursor/AggregationCursor.js | 1 - lib/cursor/QueryCursor.js | 3 +- lib/document.js | 31 +- lib/helpers/document/handleSpreadDoc.js | 20 +- lib/helpers/query/castFilterPath.js | 1 - lib/model.js | 28 +- lib/query.js | 13 +- lib/queryhelpers.js | 17 ++ lib/schema.js | 18 +- lib/schema/SubdocumentPath.js | 5 +- lib/schema/array.js | 1 + lib/schema/documentarray.js | 21 +- lib/types/array/methods/index.js | 2 + lib/virtualtype.js | 1 + package.json | 3 +- test/document.test.js | 88 +++++- test/index.test.js | 6 +- test/model.test.js | 370 ++++++++++++++++++++++++ test/query.cursor.test.js | 158 +++++----- test/types/document.test.ts | 17 ++ test/types/models.test.ts | 4 + test/types/populate.test.ts | 24 +- test/types/schema.test.ts | 22 +- types/document.d.ts | 3 + types/index.d.ts | 129 +-------- types/mongooseoptions.d.ts | 180 ++++++++++++ types/schemaoptions.d.ts | 6 + website.js | 7 + 41 files changed, 1049 insertions(+), 538 deletions(-) create mode 100644 .github/.dependabot.yml create mode 100644 types/mongooseoptions.d.ts diff --git a/.github/.dependabot.yml b/.github/.dependabot.yml new file mode 100644 index 00000000000..7792bea1c97 --- /dev/null +++ b/.github/.dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 100 + target-branch: master + ignore: + - dependency-name: "dox" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 53cff233a20..fbc8b99ebaa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: tar xf mongodb-linux-x86_64-${{ matrix.mongo-os }}-${{ matrix.mongo }}.tgz mkdir -p ./data/db/27017 ./data/db/27000 printf "\n--timeout 8000" >> ./test/mocha.opts - ./mongodb-linux-x86_64-${{ matrix.mongo-os }}-${{ matrix.mongo }}/bin/mongod --fork --dbpath ./data/db/27017 --syslog --port 27017 + ./mongodb-linux-x86_64-${{ matrix.mongo-os }}-${{ matrix.mongo }}/bin/mongod --setParameter ttlMonitorSleepSecs=1 --fork --dbpath ./data/db/27017 --syslog --port 27017 sleep 2 mongod --version echo `pwd`/mongodb-linux-x86_64-${{ matrix.mongo-os }}-${{ matrix.mongo }}/bin >> $GITHUB_PATH diff --git a/CHANGELOG.md b/CHANGELOG.md index aae33b66d8e..8742572d1a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +6.2.7 / 2022-03-16 +================== + * perf(document): avoid running validation on every array element if there's no validators to run #11380 + * fix(cursor): correctly populate in batches when batchSize is set #11509 + * fix(connection): avoid setting MongoClient on useDb() connections until after setting on base connection #11445 + * fix(schema): throw more helpful error when using schema from a different version of Mongoose module #10453 + * fix: add missing timeseries expiration handling #11489 #11229 [Uzlopak](https://github.com/Uzlopak) + * docs: correct Model.findOneAndReplace docs param naming #11524 [anatolykopyl](https://github.com/anatolykopyl) + 6.2.6 / 2022-03-11 ================== * fix(types): correct reference to cursor TypeScript bindings #11513 [SimonHausdorf](https://github.com/SimonHausdorf) diff --git a/benchmarks/index.js b/benchmarks/index.js index a11ca6430f0..c41dafc0520 100644 --- a/benchmarks/index.js +++ b/benchmarks/index.js @@ -75,7 +75,6 @@ function run(label, fn) { res.heapUsed = used.heapUsed - started.heapUsed; log('change: ', res); a = res = used = time = started = start = total = i = null; - // console.error(((used.vsize - started.vsize) / 1048576)+' MB'); } run('string', function() { diff --git a/benchmarks/validate.js b/benchmarks/validate.js index 5032a4c407b..2528c1f59bb 100644 --- a/benchmarks/validate.js +++ b/benchmarks/validate.js @@ -1,5 +1,3 @@ -// require('nodetime').profile(); - 'use strict'; const mongoose = require('../../mongoose'); @@ -25,8 +23,7 @@ const breakfastSchema = new Schema({ } }); const Breakfast = mongoose.model('Breakfast', breakfastSchema); -// const time1 = (new Date - start1); -// console.error('reading from disk and parsing JSON took %d ms', time1); + const badBreakfast = new Breakfast({ eggs: 2, bacon: 0, @@ -38,23 +35,7 @@ const goodBreakfast = new Breakfast({ bacon: 1, drink: 'Tea' }) -// const start = new Date; -// const total = 10000000; -// let i = total; -// let len; - -// for (i = 0, len = total; i < len; ++i) { - -// const goodBreakfast = new Breakfast({ -// eggs: 6, -// bacon: 1, -// drink: 'Tea' -// }) -// goodBreakfast.validateSync(); -// } -// const time = (new Date - start) / 1000; -// console.error('took %d seconds for %d docs (%d dps)', time, total, total / time); const suite = new Benchmark.Suite() suite .add('invalid async', { diff --git a/docs/images/helloclub.svg b/docs/images/helloclub.svg index d26d7f56c15..cac89ff01fc 100644 --- a/docs/images/helloclub.svg +++ b/docs/images/helloclub.svg @@ -1,16 +1,25 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/docs/loadSponsorData.js b/docs/loadSponsorData.js index e3770dd261b..958b190b22f 100644 --- a/docs/loadSponsorData.js +++ b/docs/loadSponsorData.js @@ -1,9 +1,13 @@ 'use strict'; +const axios = require('axios'); const config = require('../.config'); const fs = require('fs'); const mongoose = require('../'); +const poralHost = 'https://staging.poral.io'; +const opencollectiveUrl = `${poralHost}/invoke/${config.poralId}/generateSponsors`; + run().catch(err => { console.error(err); process.exit(-1); @@ -37,6 +41,24 @@ async function run() { } }), 'Job'); + const OpenCollectiveSponsor = mongoose.model('OpenCollectiveSponsor', mongoose.Schema({ + openCollectiveId: { + type: Number, + required: true + }, + website: { + type: String, + required: true + }, + image: { + type: String, + required: true + }, + alt: { + type: String + } + }), 'OpenCollectiveSponsor'); + try { fs.mkdirSync(`${__dirname}/data`); } catch(err) {} @@ -50,6 +72,29 @@ async function run() { const jobs = await Job.find().select({ logo: 1, company: 1, title: 1, location: 1, description: 1, url: 1 }); fs.writeFileSync(`${__dirname}/data/jobs.json`, JSON.stringify(jobs, null, ' ')); + const poralOpts = { headers: { authorization: `Basic ${config.poralToken}` } }; + const opencollectiveSponsors = await axios.post(opencollectiveUrl, {}, poralOpts). + then(res => axios.post(`${poralHost}${res.headers.location}`, {}, poralOpts)). + then(res => res.data). + catch(() => null); + + for (const sponsor of opencollectiveSponsors) { + const override = await OpenCollectiveSponsor.findOne({ openCollectiveId: sponsor['MemberId'] }); + if (override == null) { + continue; + } + + sponsor.website = override.website; + sponsor.image = override.image; + if (override.alt != null) { + sponsor.alt = override.alt; + } + } + + if (opencollectiveSponsors != null) { + fs.writeFileSync(`${__dirname}/data/opencollective.json`, JSON.stringify(opencollectiveSponsors, null, ' ')); + } + console.log('Done'); process.exit(0); } \ No newline at end of file diff --git a/docs/middleware.md b/docs/middleware.md index fa15b203a7a..e1d35b2d504 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -69,7 +69,7 @@ How pre and post hooks work is described in more detail below. **Note:** If you specify `schema.pre('remove')`, Mongoose will register this middleware for [`doc.remove()`](./api.html#model_Model-remove) by default. If you -want to your middleware to run on [`Query.remove()`](./api.html#query_Query-remove) +want your middleware to run on [`Query.remove()`](./api.html#query_Query-remove) use [`schema.pre('remove', { query: true, document: false }, fn)`](./api.html#schema_Schema-pre). **Note:** Unlike `schema.pre('remove')`, Mongoose registers `updateOne` and diff --git a/docs/migrating_to_6.md b/docs/migrating_to_6.md index f1191823959..270624e3368 100644 --- a/docs/migrating_to_6.md +++ b/docs/migrating_to_6.md @@ -39,6 +39,7 @@ If you're still on Mongoose 4.x, please read the [Mongoose 4.x to 5.x migration * [Removed Validator `isAsync`](#removed-validator-isasync) * [Removed `safe`](#removed-safe) * [SchemaType `set` parameters now use `priorValue` as the second parameter instead of `self`](#schematype-set-parameters) +* [No default model for `Query.prototype.populate()`](#no-default-model-for-query-prototype-populate) * [`toObject()` and `toJSON()` Use Nested Schema `minimize`](#toobject-and-tojson-use-nested-schema-minimize) * [TypeScript changes](#typescript-changes) @@ -461,6 +462,31 @@ const parent = new Schema({ }, { minimize: false }); ``` +

No default model for Query.prototype.populate()

+ +In Mongoose 5, calling `populate()` on a mixed type or other path with no `ref` would fall back to using the query's model. + +```javascript +const testSchema = new mongoose.Schema({ + data: String, + parents: Array // Array of mixed +}); + +const Test = mongoose.model('Test', testSchema); + +// The below `populate()`... +await Test.findOne().populate('parents'); +// Is a shorthand for the following populate in Mongoose 5 +await Test.findOne().populate({ path: 'parents', model: Test }); +``` + +In Mongoose 6, populating a path with no `ref`, `refPath`, or `model` is a no-op. + +```javascript +// The below `populate()` does nothing. +await Test.findOne().populate('parents'); +``` + ## TypeScript changes The `Schema` class now takes 3 generic params instead of 4. The 3rd generic param, `SchemaDefinitionType`, is now the same as the 1st generic param `DocType`. Replace `new Schema(schemaDefinition)` with `new Schema(schemaDefinition)` diff --git a/examples/statics/statics.js b/examples/statics/statics.js index 519d740ffb4..460b194fa86 100644 --- a/examples/statics/statics.js +++ b/examples/statics/statics.js @@ -24,7 +24,7 @@ async function run() { const result = await Person.findPersonByName('bill'); console.log(result); - cleanup(); + await cleanup(); } async function cleanup() { diff --git a/index.pug b/index.pug index 57d6240736e..ca2f9047bc8 100644 --- a/index.pug +++ b/index.pug @@ -180,240 +180,16 @@ html(lang='en') ## Sponsors - - - + div.sponsors + div + each sponsor in opencollectiveSponsors + a(rel='sponsored', href=sponsor.website, alt=sponsor.alt) + img.sponsor(src=sponsor.image || null, style='height:50px') + + p#footer Licensed under MIT. script. diff --git a/lib/browserDocument.js b/lib/browserDocument.js index 9adaeddc911..eacc2e1e631 100644 --- a/lib/browserDocument.js +++ b/lib/browserDocument.js @@ -17,6 +17,7 @@ const isObject = require('./helpers/isObject'); * Document constructor. * * @param {Object} obj the values to set + * @param {Object} schema * @param {Object} [fields] optional object containing the fields which were selected in the query returning this document and any populated paths data * @param {Boolean} [skipId] bool, should we auto create an ObjectId _id * @inherits NodeJS EventEmitter https://nodejs.org/api/events.html#events_class_events_eventemitter diff --git a/lib/connection.js b/lib/connection.js index e643e1df4e1..a73be3270a6 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -788,10 +788,6 @@ Connection.prototype.openUri = function(uri, options, callback) { } _this.client = client; - for (const db of this.otherDbs) { - _setClient(db, client, {}, db.name); - } - client.setMaxListeners(0); client.connect((error) => { if (error) { @@ -800,6 +796,10 @@ Connection.prototype.openUri = function(uri, options, callback) { _setClient(_this, client, options, dbName); + for (const db of this.otherDbs) { + _setClient(db, client, {}, db.name); + } + resolve(_this); }); }); diff --git a/lib/cursor/AggregationCursor.js b/lib/cursor/AggregationCursor.js index 1dc61b3f6d1..74e71a01598 100644 --- a/lib/cursor/AggregationCursor.js +++ b/lib/cursor/AggregationCursor.js @@ -26,7 +26,6 @@ const util = require('util'); * Use [`Aggregate#cursor()`](/docs/api.html#aggregate_Aggregate-cursor) instead. * * @param {Aggregate} agg - * @param {Object} options * @inherits Readable * @event `cursor`: Emitted when the cursor is created * @event `error`: Emitted when an error occurred diff --git a/lib/cursor/QueryCursor.js b/lib/cursor/QueryCursor.js index 247f15d5731..32e14ed3f3e 100644 --- a/lib/cursor/QueryCursor.js +++ b/lib/cursor/QueryCursor.js @@ -472,7 +472,8 @@ function _nextDoc(ctx, doc, pop, callback) { }); } - ctx.query._completeOne(doc, null, (err, doc) => { + const { model, _fields, _userProvidedFields, options } = ctx.query; + helpers.createModelAndInit(model, doc, _fields, _userProvidedFields, options, pop, (err, doc) => { if (err != null) { return callback(err); } diff --git a/lib/document.js b/lib/document.js index 7ce526fc9be..0e4e1c0a2f7 100644 --- a/lib/document.js +++ b/lib/document.js @@ -801,7 +801,7 @@ function init(self, obj, doc, opts, prefix) { init(self, obj[i], doc[i], opts, path + '.'); } else if (!schemaType) { doc[i] = obj[i]; - if (!strict) { + if (!strict && !prefix) { self[i] = obj[i]; } } else { @@ -2472,7 +2472,6 @@ function _getPathsToValidate(doc) { return true; })); - Object.keys(doc.$__.activePaths.states.init).forEach(addToPaths); Object.keys(doc.$__.activePaths.states.modify).forEach(addToPaths); Object.keys(doc.$__.activePaths.states.default).forEach(addToPaths); @@ -2484,18 +2483,23 @@ function _getPathsToValidate(doc) { if (subdoc.$basePath) { // Remove child paths for now, because we'll be validating the whole // subdoc + if (!subdoc.$__.fullPath) { + subdoc.ownerDocument(); + } + const fullPathToSubdoc = subdoc.$__.fullPath; + for (const p of paths) { - if (p === null || p.startsWith(subdoc.$basePath + '.')) { + if (p === null || p.startsWith(fullPathToSubdoc + '.')) { paths.delete(p); } } - if (doc.$isModified(subdoc.$basePath, modifiedPaths) && - !doc.isDirectModified(subdoc.$basePath) && - !doc.$isDefault(subdoc.$basePath)) { - paths.add(subdoc.$basePath); + if (doc.$isModified(fullPathToSubdoc, modifiedPaths) && + !doc.isDirectModified(fullPathToSubdoc) && + !doc.$isDefault(fullPathToSubdoc)) { + paths.add(fullPathToSubdoc); - skipSchemaValidators[subdoc.$basePath] = true; + skipSchemaValidators[fullPathToSubdoc] = true; } } } @@ -2517,6 +2521,16 @@ function _getPathsToValidate(doc) { continue; } + // gh-11380: optimization. If the array isn't a document array and there's no validators + // on the array type, there's no need to run validation on the individual array elements. + if (_pathType && + _pathType.$isMongooseArray && + !_pathType.$isMongooseDocumentArray && // Skip document arrays... + !_pathType.$embeddedSchemaType.$isMongooseArray && // and arrays of arrays + _pathType.$embeddedSchemaType.validators.length === 0) { + continue; + } + const val = doc.$__getValue(path); _pushNestedArrayPaths(val, paths, path); } @@ -2648,6 +2662,7 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { } else if (pathsToSkip) { paths = _handlePathsToSkip(paths, pathsToSkip); } + if (paths.length === 0) { return immediate(function() { const error = _complete(); diff --git a/lib/helpers/document/handleSpreadDoc.js b/lib/helpers/document/handleSpreadDoc.js index d3075e41365..63b928ee01e 100644 --- a/lib/helpers/document/handleSpreadDoc.js +++ b/lib/helpers/document/handleSpreadDoc.js @@ -2,14 +2,32 @@ const utils = require('../../utils'); +const keysToSkip = new Set(['__index', '__parentArray', '_doc']); + /** * Using spread operator on a Mongoose document gives you a * POJO that has a tendency to cause infinite recursion. So * we use this function on `set()` to prevent that. */ -module.exports = function handleSpreadDoc(v) { +module.exports = function handleSpreadDoc(v, includeExtraKeys) { if (utils.isPOJO(v) && v.$__ != null && v._doc != null) { + if (includeExtraKeys) { + const extraKeys = {}; + for (const key of Object.keys(v)) { + if (typeof key === 'symbol') { + continue; + } + if (key[0] === '$') { + continue; + } + if (keysToSkip.has(key)) { + continue; + } + extraKeys[key] = v[key]; + } + return { ...v._doc, ...extraKeys }; + } return v._doc; } diff --git a/lib/helpers/query/castFilterPath.js b/lib/helpers/query/castFilterPath.js index 42b8460ceda..0343bee8133 100644 --- a/lib/helpers/query/castFilterPath.js +++ b/lib/helpers/query/castFilterPath.js @@ -41,7 +41,6 @@ module.exports = function castFilterPath(query, schematype, val) { } continue; } - // cast(schematype.caster ? schematype.caster.schema : schema, nested, options, context); } else { val[$cond] = schematype.castForQueryWrapper({ $conditional: $cond, diff --git a/lib/model.js b/lib/model.js index 104b4a10801..fda0e476525 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1375,6 +1375,16 @@ Model.createCollection = function createCollection(options, callback) { this.schema.options.timeseries; if (timeseries != null) { options = Object.assign({ timeseries }, options); + if (options.expireAfterSeconds != null) { + // do nothing + } else if (options.expires != null) { + utils.expires(options); + } else if (this.schema.options.expireAfterSeconds != null) { + options.expireAfterSeconds = this.schema.options.expireAfterSeconds; + } else if (this.schema.options.expires != null) { + options.expires = this.schema.options.expires; + utils.expires(options); + } } callback = this.$handleCallbackError(callback); @@ -1875,7 +1885,7 @@ Model.discriminators; * ####Note: * Only translate arguments of object type anything else is returned raw * - * @param {Object} raw fields/conditions that may contain aliased keys + * @param {Object} fields fields/conditions that may contain aliased keys * @return {Object} the translated 'pure' fields/conditions */ Model.translateAliases = function translateAliases(fields) { @@ -2836,11 +2846,11 @@ Model.findByIdAndDelete = function(id, options, callback) { * * ####Examples: * - * A.findOneAndReplace(conditions, options, callback) // executes - * A.findOneAndReplace(conditions, options) // return Query - * A.findOneAndReplace(conditions, callback) // executes - * A.findOneAndReplace(conditions) // returns Query - * A.findOneAndReplace() // returns Query + * A.findOneAndReplace(filter, replacement, options, callback) // executes + * A.findOneAndReplace(filter, replacement, options) // return Query + * A.findOneAndReplace(filter, replacement, callback) // executes + * A.findOneAndReplace(filter, replacement) // returns Query + * A.findOneAndReplace() // returns Query * * @param {Object} filter Replace the first document that matches this filter * @param {Object} [replacement] Replace with this document @@ -2861,8 +2871,10 @@ Model.findOneAndReplace = function(filter, replacement, options, callback) { if (arguments.length === 1 && typeof filter === 'function') { const msg = 'Model.findOneAndReplace(): First argument must not be a function.\n\n' - + ' ' + this.modelName + '.findOneAndReplace(conditions, callback)\n' - + ' ' + this.modelName + '.findOneAndReplace(conditions)\n' + + ' ' + this.modelName + '.findOneAndReplace(filter, replacement, options, callback)\n' + + ' ' + this.modelName + '.findOneAndReplace(filter, replacement, callback)\n' + + ' ' + this.modelName + '.findOneAndReplace(filter, replacement)\n' + + ' ' + this.modelName + '.findOneAndReplace(filter, callback)\n' + ' ' + this.modelName + '.findOneAndReplace()\n'; throw new TypeError(msg); } diff --git a/lib/query.js b/lib/query.js index bf74d644660..84180dac003 100644 --- a/lib/query.js +++ b/lib/query.js @@ -3168,23 +3168,14 @@ Query.prototype._deleteMany = wrapThunk(function(callback) { */ function completeOne(model, doc, res, options, fields, userProvidedFields, pop, callback) { - const opts = pop ? - { populated: pop } - : undefined; - if (options.rawResult && doc == null) { _init(null); return null; } - const casted = helpers.createModel(model, doc, fields, userProvidedFields, options); - try { - casted.$init(doc, opts, _init); - } catch (error) { - _init(error); - } + helpers.createModelAndInit(model, doc, fields, userProvidedFields, options, pop, _init); - function _init(err) { + function _init(err, casted) { if (err) { return immediate(() => callback(err)); } diff --git a/lib/queryhelpers.js b/lib/queryhelpers.js index 1956c48791f..e4c0128080f 100644 --- a/lib/queryhelpers.js +++ b/lib/queryhelpers.js @@ -123,6 +123,23 @@ exports.createModel = function createModel(model, doc, fields, userProvidedField return new model(undefined, fields, _opts); }; +/*! + * ignore + */ + +exports.createModelAndInit = function createModelAndInit(model, doc, fields, userProvidedFields, options, populatedIds, callback) { + const initOpts = populatedIds ? + { populated: populatedIds } : + undefined; + + const casted = exports.createModel(model, doc, fields, userProvidedFields, options); + try { + casted.$init(doc, initOpts, callback); + } catch (error) { + callback(error, casted); + } +}; + /*! * ignore */ diff --git a/lib/schema.js b/lib/schema.js index 19344364f31..ba7a840df3b 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -329,6 +329,10 @@ Schema.prototype.clone = function() { return s; }; +/*! + * ignore + */ + Schema.prototype._clone = function _clone(Constructor) { Constructor = Constructor || (this.base == null ? Schema : this.base.Schema); @@ -961,8 +965,11 @@ Schema.prototype.interpretAsType = function(path, obj, options) { if (cast && cast.instanceOfSchema) { if (!(cast instanceof Schema)) { throw new TypeError('Schema for array path `' + path + - '` is from a different copy of the Mongoose module. Please make sure you\'re using the same version ' + - 'of Mongoose everywhere with `npm list mongoose`.'); + '` is from a different copy of the Mongoose module. ' + + 'Please make sure you\'re using the same version ' + + 'of Mongoose everywhere with `npm list mongoose`. If you are still ' + + 'getting this error, please add `new Schema()` around the path: ' + + `${path}: new Schema(...)`); } return new MongooseTypes.DocumentArray(path, cast, obj); } @@ -971,8 +978,11 @@ Schema.prototype.interpretAsType = function(path, obj, options) { cast[options.typeKey].instanceOfSchema) { if (!(cast[options.typeKey] instanceof Schema)) { throw new TypeError('Schema for array path `' + path + - '` is from a different copy of the Mongoose module. Please make sure you\'re using the same version ' + - 'of Mongoose everywhere with `npm list mongoose`.'); + '` is from a different copy of the Mongoose module. ' + + 'Please make sure you\'re using the same version ' + + 'of Mongoose everywhere with `npm list mongoose`. If you are still ' + + 'getting this error, please add `new Schema()` around the path: ' + + `${path}: new Schema(...)`); } return new MongooseTypes.DocumentArray(path, cast[options.typeKey], obj, cast); } diff --git a/lib/schema/SubdocumentPath.js b/lib/schema/SubdocumentPath.js index 24c2cee4f5b..3673e47710c 100644 --- a/lib/schema/SubdocumentPath.js +++ b/lib/schema/SubdocumentPath.js @@ -26,7 +26,7 @@ module.exports = SubdocumentPath; * Single nested subdocument SchemaType constructor. * * @param {Schema} schema - * @param {String} key + * @param {String} path * @param {Object} options * @inherits SchemaType * @api public @@ -243,6 +243,9 @@ SubdocumentPath.prototype.doValidate = function(value, fn, scope, options) { } if (options && options.skipSchemaValidators) { + if (!value) { + return fn(null); + } return value.validate(fn); } diff --git a/lib/schema/array.js b/lib/schema/array.js index 5e71b5bcd73..70be9cdcfc4 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -32,6 +32,7 @@ const emptyOpts = Object.freeze({}); * @param {String} key * @param {SchemaType} cast * @param {Object} options + * @param {Object} schemaOptions * @inherits SchemaType * @api public */ diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js index 3bb0b96b55e..3818677d129 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -12,6 +12,7 @@ const SchemaDocumentArrayOptions = const SchemaType = require('../schematype'); const discriminator = require('../helpers/model/discriminator'); const handleIdOption = require('../helpers/schema/handleIdOption'); +const handleSpreadDoc = require('../helpers/document/handleSpreadDoc'); const util = require('util'); const utils = require('../utils'); const getConstructor = require('../helpers/discriminator/getConstructor'); @@ -29,6 +30,7 @@ let Subdocument; * @param {String} key * @param {Schema} schema * @param {Object} options + * @param {Object} schemaOptions * @inherits SchemaArray * @api public */ @@ -427,13 +429,18 @@ DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) { const Constructor = getConstructor(this.casterConstructor, rawArray[i]); // Check if the document has a different schema (re gh-3701) - if (rawArray[i].$__ && !(rawArray[i] instanceof Constructor)) { - rawArray[i] = rawArray[i].toObject({ - transform: false, - // Special case: if different model, but same schema, apply virtuals - // re: gh-7898 - virtuals: rawArray[i].schema === Constructor.schema - }); + if (rawArray[i].$__ != null && !(rawArray[i] instanceof Constructor)) { + const spreadDoc = handleSpreadDoc(rawArray[i], true); + if (rawArray[i] !== spreadDoc) { + rawArray[i] = spreadDoc; + } else { + rawArray[i] = rawArray[i].toObject({ + transform: false, + // Special case: if different model, but same schema, apply virtuals + // re: gh-7898 + virtuals: rawArray[i].schema === Constructor.schema + }); + } } if (rawArray[i] instanceof Subdocument) { diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js index cc16cbbf33f..cfb3a6a2d63 100644 --- a/lib/types/array/methods/index.js +++ b/lib/types/array/methods/index.js @@ -445,6 +445,7 @@ const methods = { * Return whether or not the `obj` is included in the array. * * @param {Object} obj the item to check + * @param {Number} fromIndex * @return {Boolean} * @api public * @method includes @@ -460,6 +461,7 @@ const methods = { * Return the index of `obj` or `-1` if not found. * * @param {Object} obj the item to look for + * @param {Number} fromIndex * @return {Number} * @api public * @method indexOf diff --git a/lib/virtualtype.js b/lib/virtualtype.js index 58a44b59c4f..27756630ba7 100644 --- a/lib/virtualtype.js +++ b/lib/virtualtype.js @@ -24,6 +24,7 @@ const utils = require('./utils'); * @param {Number} [options.skip=null] add a default `skip` to the `populate()` query * @param {Number} [options.perDocumentLimit=null] For legacy reasons, `limit` with `populate()` may give incorrect results because it only executes a single query for every document being populated. If you set `perDocumentLimit`, Mongoose will ensure correct `limit` per document by executing a separate query for each document to `populate()`. For example, `.find().populate({ path: 'test', perDocumentLimit: 2 })` will execute 2 additional queries if `.find()` returns 2 documents. * @param {Object} [options.options=null] Additional options like `limit` and `lean`. + * @param {string} name * @api public */ diff --git a/package.json b/package.json index 30ba6fd49ab..1600ad01a04 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.2.6", + "version": "6.2.7", "author": "Guillermo Rauch ", "keywords": [ "mongodb", @@ -35,6 +35,7 @@ "acquit": "1.x", "acquit-ignore": "0.2.x", "acquit-require": "0.1.x", + "axios": "0.26.1", "babel-loader": "8.2.3", "benchmark": "2.1.4", "bluebird": "3.7.2", diff --git a/test/document.test.js b/test/document.test.js index 0e35cc9c607..10dde42e7b8 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -11122,12 +11122,88 @@ describe('document', function() { assert.ok(!reloaded.nested.foo.$__isNested); assert.strictEqual(reloaded.nested.foo.bar, 66); }); - describe('Check if a document instance function that is supplied in schema option is availabe (m0_0a)', function() { - it('should give an instance function back rather than undefined', 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'); + + it('saves changes when setting a nested path to itself (gh-11395)', async function() { + const Test = db.model('Test', new Schema({ + co: { value: Number } + })); + + await Test.create({}); + + const doc = await Test.findOne(); + doc.co.value = 123; + doc.co = doc.co; + await doc.save(); + + const res = await Test.findById(doc._id); + assert.strictEqual(res.co.value, 123); + }); + + it('avoids setting nested properties on top-level document when init-ing with strict: false (gh-11526) (gh-11309)', async function() { + const testSchema = Schema({ name: String }, { strict: false, strictQuery: false }); + const Test = db.model('Test', testSchema); + + const doc = new Test(); + doc.init({ + details: { + person: { + name: 'Baz' + } + } + }); + + assert.strictEqual(doc.name, void 0); + }); + + it('handles deeply nested subdocuments when getting paths to validate (gh-11501)', async function() { + const schema = Schema({ + parameters: { + test: { + type: new Schema({ + value: 'Mixed' + }) + } + }, + nested: Schema({ + parameters: { + type: Map, + of: Schema({ + value: 'Mixed' + }) + } + }) + }); + const Test = db.model('Test', schema); + + await Test.create({ + nested: { + parameters: new Map([['test', { answer: 42 }]]) + } }); }); + + it('handles casting array of spread documents (gh-11522)', async function() { + const Test = db.model('Test', new Schema({ + arr: [{ _id: false, prop1: String, prop2: String }] + })); + + const doc = new Test({ arr: [{ prop1: 'test' }] }); + + doc.arr = doc.arr.map(member => ({ + ...member, + prop2: 'foo' + })); + + assert.deepStrictEqual(doc.toObject().arr, [{ prop1: 'test', prop2: 'foo' }]); + + 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/index.test.js b/test/index.test.js index 51a47c3b8e8..abfa2a0a719 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -773,7 +773,7 @@ describe('mongoose module:', function() { const movie = await Movie.create({ name: 'The Empire Strikes Back' }); await Person.create({ name: 'Test1', favoriteMovie: movie._id }); - assert.rejects(async() => { + await assert.rejects(async() => { await Person.findOne().populate({ path: 'favoriteGame' }); }, { message: 'Cannot populate path `favoriteGame` because it is not in your schema. Set the `strictPopulate` option to false to override.' }); }); @@ -788,7 +788,7 @@ describe('mongoose module:', function() { })); const movie = await Movie.create({ name: 'The Empire Strikes Back' }); await Person.create({ name: 'Test1', favoriteMovie: movie._id }); - assert.rejects(async() => { + await assert.rejects(async() => { await Person.findOne().populate({ path: 'favoriteGame', strictPopulate: true }); }, { message: 'Cannot populate path `favoriteGame` because it is not in your schema. Set the `strictPopulate` option to false to override.' }); }); @@ -912,4 +912,4 @@ describe('mongoose module:', function() { assert.deepEqual(await m.syncIndexes(), {}); }); }); -}); \ No newline at end of file +}); diff --git a/test/model.test.js b/test/model.test.js index d9f2f383790..517e3f8a3a7 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -6809,6 +6809,376 @@ describe('Model', function() { await Test.collection.drop().catch(() => {}); }); + it('createCollection() enforces expireAfterSeconds (gh-11229)', async function() { + this.timeout(10000); + const version = await start.mongodVersion(); + if (version[0] < 5) { + this.skip(); + return; + } + + const schema = Schema({ name: String, timestamp: Date, metadata: Object }, { + timeseries: { + timeField: 'timestamp', + metaField: 'metadata', + granularity: 'hours' + }, + autoCreate: false + }); + + const Test = db.model('TestGH11229Var1', schema); + + await Test.collection.drop().catch(() => {}); + await Test.createCollection({ expireAfterSeconds: 5 }); + + await Test.insertMany([ + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T00:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T04:00:00.000Z'), + temp: 11 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T08:00:00.000Z'), + temp: 11 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T12:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T16:00:00.000Z'), + temp: 16 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T20:00:00.000Z'), + temp: 15 + }, { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T00:00:00.000Z'), + temp: 13 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T04:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T08:00:00.000Z'), + temp: 11 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T12:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T16:00:00.000Z'), + temp: 17 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T20:00:00.000Z'), + temp: 12 + } + ]); + + const beforeExpirationCount = await Test.count({}); + assert.ok(beforeExpirationCount === 12); + await new Promise(resolve => setTimeout(resolve, 6000)); + const afterExpirationCount = await Test.count({}); + assert.ok(afterExpirationCount === 0); + await Test.collection.drop().catch(() => {}); + }); + + it('createCollection() enforces expires (gh-11229)', async function() { + this.timeout(10000); + const version = await start.mongodVersion(); + if (version[0] < 5) { + this.skip(); + return; + } + + const schema = Schema({ name: String, timestamp: Date, metadata: Object }, { + timeseries: { + timeField: 'timestamp', + metaField: 'metadata', + granularity: 'hours' + }, + autoCreate: false + }); + + const Test = db.model('TestGH11229Var2', schema); + + await Test.collection.drop().catch(() => {}); + await Test.createCollection({ expires: '5 seconds' }); + + await Test.insertMany([ + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T00:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T04:00:00.000Z'), + temp: 11 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T08:00:00.000Z'), + temp: 11 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T12:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T16:00:00.000Z'), + temp: 16 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T20:00:00.000Z'), + temp: 15 + }, { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T00:00:00.000Z'), + temp: 13 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T04:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T08:00:00.000Z'), + temp: 11 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T12:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T16:00:00.000Z'), + temp: 17 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T20:00:00.000Z'), + temp: 12 + } + ]); + + const beforeExpirationCount = await Test.count({}); + assert.ok(beforeExpirationCount === 12); + await new Promise(resolve => setTimeout(resolve, 6000)); + const afterExpirationCount = await Test.count({}); + assert.ok(afterExpirationCount === 0); + await Test.collection.drop().catch(() => {}); + }); + + it('createCollection() enforces expireAfterSeconds when set by Schema (gh-11229)', async function() { + this.timeout(10000); + const version = await start.mongodVersion(); + if (version[0] < 5) { + this.skip(); + return; + } + + const schema = Schema({ name: String, timestamp: Date, metadata: Object }, { + timeseries: { + timeField: 'timestamp', + metaField: 'metadata', + granularity: 'hours' + }, + autoCreate: false, + expireAfterSeconds: 5 + }); + + const Test = db.model('TestGH11229Var3', schema); + + await Test.collection.drop().catch(() => {}); + await Test.createCollection(); + + await Test.insertMany([ + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T00:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T04:00:00.000Z'), + temp: 11 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T08:00:00.000Z'), + temp: 11 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T12:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T16:00:00.000Z'), + temp: 16 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T20:00:00.000Z'), + temp: 15 + }, { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T00:00:00.000Z'), + temp: 13 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T04:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T08:00:00.000Z'), + temp: 11 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T12:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T16:00:00.000Z'), + temp: 17 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T20:00:00.000Z'), + temp: 12 + } + ]); + + const beforeExpirationCount = await Test.count({}); + assert.ok(beforeExpirationCount === 12); + await new Promise(resolve => setTimeout(resolve, 6000)); + const afterExpirationCount = await Test.count({}); + assert.ok(afterExpirationCount === 0); + await Test.collection.drop().catch(() => {}); + }); + + it('createCollection() enforces expires when set by Schema (gh-11229)', async function() { + this.timeout(10000); + const version = await start.mongodVersion(); + if (version[0] < 5) { + this.skip(); + return; + } + + const schema = Schema({ name: String, timestamp: Date, metadata: Object }, { + timeseries: { + timeField: 'timestamp', + metaField: 'metadata', + granularity: 'hours' + }, + autoCreate: false, + expires: '5 seconds' + }); + + const Test = db.model('TestGH11229Var4', schema); + + await Test.collection.drop().catch(() => {}); + await Test.createCollection(); + + await Test.insertMany([ + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T00:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T04:00:00.000Z'), + temp: 11 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T08:00:00.000Z'), + temp: 11 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T12:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T16:00:00.000Z'), + temp: 16 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-18T20:00:00.000Z'), + temp: 15 + }, { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T00:00:00.000Z'), + temp: 13 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T04:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T08:00:00.000Z'), + temp: 11 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T12:00:00.000Z'), + temp: 12 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T16:00:00.000Z'), + temp: 17 + }, + { + metadata: { sensorId: 5578, type: 'temperature' }, + timestamp: new Date('2021-05-19T20:00:00.000Z'), + temp: 12 + } + ]); + + const beforeExpirationCount = await Test.count({}); + assert.ok(beforeExpirationCount === 12); + await new Promise(resolve => setTimeout(resolve, 6000)); + const afterExpirationCount = await Test.count({}); + assert.ok(afterExpirationCount === 0); + await Test.collection.drop().catch(() => {}); + }); + it('createCollection() handles NamespaceExists errors (gh-9447)', async function() { const userSchema = new Schema({ name: String }); const Model = db.model('User', userSchema); diff --git a/test/query.cursor.test.js b/test/query.cursor.test.js index 4a853327b8d..575d06e79b6 100644 --- a/test/query.cursor.test.js +++ b/test/query.cursor.test.js @@ -108,6 +108,8 @@ describe('QueryCursor', function() { }); describe('with populate', function() { + let populateCalls = 0; + const bandSchema = new Schema({ name: String, members: [{ type: mongoose.Schema.ObjectId, ref: 'Person' }] @@ -115,10 +117,11 @@ describe('QueryCursor', function() { const personSchema = new Schema({ name: String }); + personSchema.pre('find', () => { ++populateCalls; }); let Band; - beforeEach(function(done) { + beforeEach(async function() { const Person = db.model('Person', personSchema); Band = db.model('Band', bandSchema); @@ -131,96 +134,81 @@ describe('QueryCursor', function() { { name: 'Thom Yorke' }, { name: 'Billy Corgan' } ]; - Person.create(people, function(error, docs) { - assert.ifError(error); - const bands = [ - { name: 'Guns N\' Roses', members: [docs[0], docs[1]] }, - { name: 'Motley Crue', members: [docs[2], docs[3]] }, - { name: 'Nine Inch Nails', members: [docs[4]] }, - { name: 'Radiohead', members: [docs[5]] }, - { name: 'The Smashing Pumpkins', members: [docs[6]] } - ]; - Band.create(bands, function(error) { - assert.ifError(error); - done(); - }); - }); + const docs = await Person.create(people); + + const bands = [ + { name: 'Guns N\' Roses', members: [docs[0], docs[1]] }, + { name: 'Motley Crue', members: [docs[2], docs[3]] }, + { name: 'Nine Inch Nails', members: [docs[4]] }, + { name: 'Radiohead', members: [docs[5]] }, + { name: 'The Smashing Pumpkins', members: [docs[6]] } + ]; + await Band.create(bands); }); - it('with populate without specify batchSize', function(done) { - const cursor = - Band.find().sort({ name: 1 }).populate('members').cursor(); - cursor.next(function(error, doc) { - assert.ifError(error); - assert.equal(doc.name, 'Guns N\' Roses'); - assert.equal(doc.members.length, 2); - assert.equal(doc.members[0].name, 'Axl Rose'); - assert.equal(doc.members[1].name, 'Slash'); - cursor.next(function(error, doc) { - assert.ifError(error); - assert.equal(doc.name, 'Motley Crue'); - assert.equal(doc.members.length, 2); - assert.equal(doc.members[0].name, 'Nikki Sixx'); - assert.equal(doc.members[1].name, 'Vince Neil'); - cursor.next(function(error, doc) { - assert.ifError(error); - assert.equal(doc.name, 'Nine Inch Nails'); - assert.equal(doc.members.length, 1); - assert.equal(doc.members[0].name, 'Trent Reznor'); - cursor.next(function(error, doc) { - assert.ifError(error); - assert.equal(doc.name, 'Radiohead'); - assert.equal(doc.members.length, 1); - assert.equal(doc.members[0].name, 'Thom Yorke'); - cursor.next(function(error, doc) { - assert.ifError(error); - assert.equal(doc.name, 'The Smashing Pumpkins'); - assert.equal(doc.members.length, 1); - assert.equal(doc.members[0].name, 'Billy Corgan'); - done(); - }); - }); - }); - }); - }); + it('with populate without specify batchSize', async function() { + const cursor = Band.find().sort({ name: 1 }).populate('members').cursor(); + + let doc = await cursor.next(); + assert.equal(doc.name, 'Guns N\' Roses'); + assert.equal(doc.members.length, 2); + assert.equal(doc.members[0].name, 'Axl Rose'); + assert.equal(doc.members[1].name, 'Slash'); + + doc = await cursor.next(); + assert.equal(doc.name, 'Motley Crue'); + assert.equal(doc.members.length, 2); + assert.equal(doc.members[0].name, 'Nikki Sixx'); + assert.equal(doc.members[1].name, 'Vince Neil'); + + doc = await cursor.next(); + assert.equal(doc.name, 'Nine Inch Nails'); + assert.equal(doc.members.length, 1); + assert.equal(doc.members[0].name, 'Trent Reznor'); + + doc = await cursor.next(); + assert.equal(doc.name, 'Radiohead'); + assert.equal(doc.members.length, 1); + assert.equal(doc.members[0].name, 'Thom Yorke'); + + doc = await cursor.next(); + assert.equal(doc.name, 'The Smashing Pumpkins'); + assert.equal(doc.members.length, 1); + assert.equal(doc.members[0].name, 'Billy Corgan'); }); - it('with populate using custom batchSize', function(done) { + it('with populate using custom batchSize', async function() { + populateCalls = 0; const cursor = Band.find().sort({ name: 1 }).populate('members').batchSize(3).cursor(); - cursor.next(function(error, doc) { - assert.ifError(error); - assert.equal(doc.name, 'Guns N\' Roses'); - assert.equal(doc.members.length, 2); - assert.equal(doc.members[0].name, 'Axl Rose'); - assert.equal(doc.members[1].name, 'Slash'); - cursor.next(function(error, doc) { - assert.ifError(error); - assert.equal(doc.name, 'Motley Crue'); - assert.equal(doc.members.length, 2); - assert.equal(doc.members[0].name, 'Nikki Sixx'); - assert.equal(doc.members[1].name, 'Vince Neil'); - cursor.next(function(error, doc) { - assert.ifError(error); - assert.equal(doc.name, 'Nine Inch Nails'); - assert.equal(doc.members.length, 1); - assert.equal(doc.members[0].name, 'Trent Reznor'); - cursor.next(function(error, doc) { - assert.ifError(error); - assert.equal(doc.name, 'Radiohead'); - assert.equal(doc.members.length, 1); - assert.equal(doc.members[0].name, 'Thom Yorke'); - cursor.next(function(error, doc) { - assert.ifError(error); - assert.equal(doc.name, 'The Smashing Pumpkins'); - assert.equal(doc.members.length, 1); - assert.equal(doc.members[0].name, 'Billy Corgan'); - done(); - }); - }); - }); - }); - }); + + let doc = await cursor.next(); + assert.equal(doc.name, 'Guns N\' Roses'); + assert.equal(doc.members.length, 2); + assert.equal(doc.members[0].name, 'Axl Rose'); + assert.equal(doc.members[1].name, 'Slash'); + + doc = await cursor.next(); + assert.equal(doc.name, 'Motley Crue'); + assert.equal(doc.members.length, 2); + assert.equal(doc.members[0].name, 'Nikki Sixx'); + assert.equal(doc.members[1].name, 'Vince Neil'); + + doc = await cursor.next(); + assert.equal(doc.name, 'Nine Inch Nails'); + assert.equal(doc.members.length, 1); + assert.equal(doc.members[0].name, 'Trent Reznor'); + + doc = await cursor.next(); + assert.equal(doc.members.length, 1); + assert.equal(doc.members[0].name, 'Thom Yorke'); + + doc = await cursor.next(); + assert.equal(doc.name, 'The Smashing Pumpkins'); + assert.equal(doc.members.length, 1); + assert.equal(doc.members[0].name, 'Billy Corgan'); + + assert.equal(populateCalls, 2); }); }); diff --git a/test/types/document.test.ts b/test/types/document.test.ts index 3719976acd4..6aa4c8f1b2f 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -153,4 +153,21 @@ function gh11085(): void { let _id: number; expectError(_id = newUser._id); const _id2: Types.ObjectId = newUser._id; +} + +function gh11435() { + interface Item { + name: string; + } + const ItemSchema = new Schema({ name: String }); + + ItemSchema.pre('validate', function preValidate() { + 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/models.test.ts b/test/types/models.test.ts index 051fcfc7a77..ad8826af535 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -208,6 +208,10 @@ function inheritance() { } } +Project.createCollection({ expires: '5 seconds' }); +Project.createCollection({ expireAfterSeconds: 5 }); +expectError(Project.createCollection({ expireAfterSeconds: '5 seconds' })); + export async function m0_0aModel() { const AutoTypeSchema = m0_0aSchema(); const AutoTypeModel = model('AutoTypeModel', AutoTypeSchema); diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts index 9a744ed83dd..6933eff6f63 100644 --- a/test/types/populate.test.ts +++ b/test/types/populate.test.ts @@ -164,21 +164,37 @@ function gh11503() { interface User { friends: Types.ObjectId[]; } - const UserSchema = new Schema({ + const userSchema = new Schema({ friends: [{ type: Schema.Types.ObjectId, ref: 'friends' }] }); - const Users = model('friends', UserSchema); + const User = model('friends', userSchema); - Users.findOne({}).populate('friends').then(user => { + User.findOne({}).populate('friends').then(user => { expectType(user?.friends[0]); expectError(user?.friends[0].blocked); expectError(user?.friends.map(friend => friend.blocked)); }); - Users.findOne({}).populate<{friends: Friend[]}>('friends').then(user => { + User.findOne({}).populate<{friends: Friend[]}>('friends').then(user => { expectAssignable(user?.friends[0]); expectType(user?.friends[0].blocked); const firstFriendBlockedValue = user?.friends.map(friend => friend)[0]; expectType(firstFriendBlockedValue?.blocked); }); +} + + +function gh11544() { + + interface User { + friends: Types.ObjectId[]; + } + const userSchema = new Schema({ + friends: [{ type: Schema.Types.ObjectId, ref: 'friends' }] + }); + const User = model('friends', userSchema); + + User.findOne({}).populate({ path: 'friends', strictPopulate: false }); + User.findOne({}).populate({ path: 'friends', strictPopulate: true }); + User.findOne({}).populate({ path: 'friends', populate: { path: 'someNestedPath', strictPopulate: false } }); } \ No newline at end of file diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 65c3a525aa5..a1fccfb35b2 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1,5 +1,5 @@ -import { Schema, Document, SchemaDefinition, Model, InferSchemaType, SchemaType } from 'mongoose'; -import { expectError, expectType } from 'tsd'; +import { Schema, Document, SchemaDefinition, Model, Types, SchemaType, InferSchemaType } from 'mongoose'; +import { expectType, expectError } from 'tsd'; enum Genre { Action, @@ -288,6 +288,24 @@ function gh11448() { userSchema.pick>(['age']); } +function gh11435(): void { + interface User { + ids: Types.Array; + } + + const schema = new Schema({ + ids: { + type: [{ type: Schema.Types.ObjectId, ref: 'Something' }], + default: [] + } + }); +} + +// timeSeries +new Schema({}, { expires: '5 seconds' }); +expectError(new Schema({}, { expireAfterSeconds: '5 seconds' })); +new Schema({}, { expireAfterSeconds: 5 }); + export type M0_0aAutoTypedSchemaType = { schema: { userName: string; diff --git a/types/document.d.ts b/types/document.d.ts index efd9b76dc91..d889a019ce0 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -169,6 +169,9 @@ declare module 'mongoose' { /** The name of the model */ modelName: string; + /** Returns the model with the given name on this document's associated connection. */ + model>(name: string): ModelType; + /** * Overwrite all values in this document with the values of `obj`, except * for immutable properties. Behaves similarly to `set()`, except for it diff --git a/types/index.d.ts b/types/index.d.ts index 4908466805f..8ac0b20c5a1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2,6 +2,7 @@ /// /// /// +/// /// /// /// @@ -9,7 +10,6 @@ import events = require('events'); import mongodb = require('mongodb'); import mongoose = require('mongoose'); -import stream = require('stream'); declare module 'mongoose' { @@ -177,117 +177,6 @@ export function model( type Mongoose = typeof mongoose; - interface MongooseOptions { - /** true by default. Set to false to skip applying global plugins to child schemas */ - applyPluginsToChildSchemas?: boolean; - - /** - * false by default. Set to true to apply global plugins to discriminator schemas. - * This typically isn't necessary because plugins are applied to the base schema and - * discriminators copy all middleware, methods, statics, and properties from the base schema. - */ - applyPluginsToDiscriminators?: boolean; - - /** - * Set to `true` to make Mongoose call` Model.createCollection()` automatically when you - * create a model with `mongoose.model()` or `conn.model()`. This is useful for testing - * transactions, change streams, and other features that require the collection to exist. - */ - autoCreate?: boolean; - - /** - * true by default. Set to false to disable automatic index creation - * for all models associated with this Mongoose instance. - */ - autoIndex?: boolean; - - /** enable/disable mongoose's buffering mechanism for all connections and models */ - bufferCommands?: boolean; - - bufferTimeoutMS?: number; - - /** false by default. Set to `true` to `clone()` all schemas before compiling into a model. */ - cloneSchemas?: boolean; - - /** - * If `true`, prints the operations mongoose sends to MongoDB to the console. - * If a writable stream is passed, it will log to that stream, without colorization. - * If a callback function is passed, it will receive the collection name, the method - * name, then all arguments passed to the method. For example, if you wanted to - * replicate the default logging, you could output from the callback - * `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`. - */ - debug?: - | boolean - | { color?: boolean; shell?: boolean } - | stream.Writable - | ((collectionName: string, methodName: string, ...methodArgs: any[]) => void); - - /** If set, attaches [maxTimeMS](https://docs.mongodb.com/manual/reference/operator/meta/maxTimeMS/) to every query */ - maxTimeMS?: number; - - /** - * true by default. Mongoose adds a getter to MongoDB ObjectId's called `_id` that - * returns `this` for convenience with populate. Set this to false to remove the getter. - */ - objectIdGetter?: boolean; - - /** - * Set to `true` to default to overwriting models with the same name when calling - * `mongoose.model()`, as opposed to throwing an `OverwriteModelError`. - */ - overwriteModels?: boolean; - - /** - * If `false`, changes the default `returnOriginal` option to `findOneAndUpdate()`, - * `findByIdAndUpdate`, and `findOneAndReplace()` to false. This is equivalent to - * setting the `new` option to `true` for `findOneAndX()` calls by default. Read our - * `findOneAndUpdate()` [tutorial](https://mongoosejs.com/docs/tutorials/findoneandupdate.html) - * for more information. - */ - returnOriginal?: boolean; - - /** - * false by default. Set to true to enable [update validators]( - * https://mongoosejs.com/docs/validation.html#update-validators - * ) for all validators by default. - */ - runValidators?: boolean; - - sanitizeFilter?: boolean; - - sanitizeProjection?: boolean; - - /** - * true by default. Set to false to opt out of Mongoose adding all fields that you `populate()` - * to your `select()`. The schema-level option `selectPopulatedPaths` overwrites this one. - */ - selectPopulatedPaths?: boolean; - - setDefaultsOnInsert?: boolean; - - /** true by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas. */ - strict?: boolean | 'throw'; - - /** true by default. set to `false` to allow populating paths that aren't in the schema */ - strictPopulate?: boolean; - - /** - * false by default, may be `false`, `true`, or `'throw'`. Sets the default - * [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery) mode for schemas. - */ - strictQuery?: boolean | 'throw'; - - /** - * `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to - * `toJSON()`, for determining how Mongoose documents get serialized by `JSON.stringify()` - */ - toJSON?: ToObjectOptions; - - /** `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to `toObject()` */ - toObject?: ToObjectOptions; - } - // eslint-disable-next-line @typescript-eslint/no-empty-interface interface ClientSession extends mongodb.ClientSession { } @@ -420,8 +309,8 @@ export function model( * 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): Promise>; - createCollection(options: mongodb.CreateCollectionOptions | null, callback: Callback>): void; + createCollection(options?: mongodb.CreateCollectionOptions & Pick): Promise>; + createCollection(options: mongodb.CreateCollectionOptions & Pick | null, callback: Callback>): void; /** * Similar to `ensureIndexes()`, except for it uses the [`createIndex`](http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#createIndex) @@ -819,6 +708,8 @@ export function model( model?: string | Model; /** optional query options like sort, limit, etc */ options?: any; + /** optional boolean, set to `false` to allow populating paths that aren't in the schema */ + strictPopulate?: boolean; /** deep populate */ populate?: string | PopulateOptions | (string | PopulateOptions)[]; /** @@ -867,9 +758,9 @@ export function model( 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 = any, + class Schema, TInstanceMethods = {}, TQueryHelpers = {}, TPathTypeKey extends TypeKeyBaseType = DefaultTypeKey, - DocType extends ObtainDocumentType = any, + DocType extends ObtainDocumentType = ObtainDocumentType, TStaticMethods = {}> extends events.EventEmitter { /** @@ -925,7 +816,7 @@ export function model( method(obj: Partial): this; /** Object of currently defined methods on this schema. */ - methods: any; + methods: { [F in keyof TInstanceMethods]: TInstanceMethods[F] } & AnyObject; /** The original object passed to the schema constructor */ obj: SchemaDefinition>; @@ -1047,6 +938,8 @@ export function model( type AnyArray = T[] | ReadonlyArray; type SchemaValidator = RegExp | [RegExp, string] | Function | [Function, string] | ValidateOpts | ValidateOpts[]; + type ExtractMongooseArray = T extends Types.Array ? AnyArray> : T; + export class SchemaTypeOptions { type?: T extends string ? StringSchemaDefinition : @@ -1084,7 +977,7 @@ export function model( * The default value for this path. If a function, Mongoose executes the function * and uses the return value as the default. */ - default?: T | ((this: any, doc: any) => Partial); + default?: ExtractMongooseArray | ((this: any, doc: any) => Partial>); /** * The model that `populate()` should use if populating this path. diff --git a/types/mongooseoptions.d.ts b/types/mongooseoptions.d.ts new file mode 100644 index 00000000000..89a8c897ea1 --- /dev/null +++ b/types/mongooseoptions.d.ts @@ -0,0 +1,180 @@ +import stream = require('stream'); + +declare module 'mongoose' { + + interface MongooseOptions { + /** + * Set to false to skip applying global plugins to child schemas. + * + * @default true + */ + applyPluginsToChildSchemas?: boolean; + + /** + * Set to true to apply global plugins to discriminator schemas. + * This typically isn't necessary because plugins are applied to the base schema and + * discriminators copy all middleware, methods, statics, and properties from the base schema. + * + * @default false + */ + applyPluginsToDiscriminators?: boolean; + + /** + * autoCreate is true by default unless readPreference is secondary or secondaryPreferred, + * which means Mongoose will attempt to create every model's underlying collection before + * creating indexes. If readPreference is secondary or secondaryPreferred, Mongoose will + * default to false for both autoCreate and autoIndex because both createCollection() and + * createIndex() will fail when connected to a secondary. + */ + autoCreate?: boolean; + + /** + * Set to false to disable automatic index creation for all models associated with this Mongoose instance. + * + * @default true + */ + autoIndex?: boolean; + + /** + * enable/disable mongoose's buffering mechanism for all connections and models. + * + * @default true + */ + bufferCommands?: boolean; + + /** + * If bufferCommands is on, this option sets the maximum amount of time Mongoose + * buffering will wait before throwing an error. + * If not specified, Mongoose will use 10000 (10 seconds). + * + * @default 10000 + */ + bufferTimeoutMS?: number; + + /** + * Set to `true` to `clone()` all schemas before compiling into a model. + * + * @default false + */ + cloneSchemas?: boolean; + + /** + * If `true`, prints the operations mongoose sends to MongoDB to the console. + * If a writable stream is passed, it will log to that stream, without colorization. + * If a callback function is passed, it will receive the collection name, the method + * name, then all arguments passed to the method. For example, if you wanted to + * replicate the default logging, you could output from the callback + * `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`. + * + * @default false + */ + debug?: + | boolean + | { color?: boolean; shell?: boolean; } + | stream.Writable + | ((collectionName: string, methodName: string, ...methodArgs: any[]) => void); + + /** If set, attaches [maxTimeMS](https://docs.mongodb.com/manual/reference/operator/meta/maxTimeMS/) to every query */ + maxTimeMS?: number; + + /** + * Mongoose adds a getter to MongoDB ObjectId's called `_id` that + * returns `this` for convenience with populate. Set this to false to remove the getter. + * + * @default true + */ + objectIdGetter?: boolean; + + /** + * Set to `true` to default to overwriting models with the same name when calling + * `mongoose.model()`, as opposed to throwing an `OverwriteModelError`. + * + * @default false + */ + overwriteModels?: boolean; + + /** + * If `false`, changes the default `returnOriginal` option to `findOneAndUpdate()`, + * `findByIdAndUpdate`, and `findOneAndReplace()` to false. This is equivalent to + * setting the `new` option to `true` for `findOneAndX()` calls by default. Read our + * `findOneAndUpdate()` [tutorial](https://mongoosejs.com/docs/tutorials/findoneandupdate.html) + * for more information. + * + * @default true + */ + returnOriginal?: boolean; + + /** + * Set to true to enable [update validators]( + * https://mongoosejs.com/docs/validation.html#update-validators + * ) for all validators by default. + * + * @default false + */ + runValidators?: boolean; + + /** + * Sanitizes query filters against [query selector injection attacks]( + * https://thecodebarbarian.com/2014/09/04/defending-against-query-selector-injection-attacks.html + * ) by wrapping any nested objects that have a property whose name starts with $ in a $eq. + */ + sanitizeFilter?: boolean; + + sanitizeProjection?: boolean; + + /** + * Set to false to opt out of Mongoose adding all fields that you `populate()` + * to your `select()`. The schema-level option `selectPopulatedPaths` overwrites this one. + * + * @default true + */ + selectPopulatedPaths?: boolean; + + /** + * Mongoose also sets defaults on update() and findOneAndUpdate() when the upsert option is + * set by adding your schema's defaults to a MongoDB $setOnInsert operator. You can disable + * this behavior by setting the setDefaultsOnInsert option to false. + * + * @default true + */ + setDefaultsOnInsert?: boolean; + + /** + * Sets the default strict mode for schemas. + * May be `false`, `true`, or `'throw'`. + * + * @default true + */ + strict?: boolean | 'throw'; + + /** + * Set to `false` to allow populating paths that aren't in the schema. + * + * @default true + */ + strictPopulate?: boolean; + + /** + * Sets the default [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery) mode for schemas. + * May be `false`, `true`, or `'throw'`. + * + * @default false + */ + strictQuery?: boolean | 'throw'; + + /** + * Overwrites default objects to `toJSON()`, for determining how Mongoose + * documents get serialized by `JSON.stringify()` + * + * @default { transform: true, flattenDecimals: true } + */ + toJSON?: ToObjectOptions; + + /** + * Overwrites default objects to `toObject()` + * + * @default { transform: true, flattenDecimals: true } + */ + toObject?: ToObjectOptions; + } +} \ No newline at end of file diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 0adfee71f9f..b9863ed52cc 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -47,6 +47,12 @@ declare module 'mongoose' { /** The timeseries option to use when creating the model's collection. */ timeseries?: mongodb.TimeSeriesCollectionOptions; + /** The number of seconds after which a document in a timeseries collection expires. */ + expireAfterSeconds?: number; + + /** The time after which a document in a timeseries collection expires. */ + expires?: number | string; + /** * Mongoose by default produces a collection name by passing the model name to the utils.toCollectionName * method. This method pluralizes the name. Set this option if you need a different name for your collection. diff --git a/website.js b/website.js index 24fade2a39c..06f6635a96d 100644 --- a/website.js +++ b/website.js @@ -17,6 +17,11 @@ try { jobs = require('./docs/data/jobs.json'); } catch (err) {} +let opencollectiveSponsors = []; +try { + opencollectiveSponsors = require('./docs/data/opencollective.json'); +} catch (err) {} + require('acquit-ignore')(); const markdown = require('marked'); @@ -162,6 +167,8 @@ function pugify(filename, options, newfile) { options.outputUrl = newfile.replace(process.cwd(), ''); options.jobs = jobs; + options.opencollectiveSponsors = opencollectiveSponsors; + pug.render(contents, options, function(err, str) { if (err) { console.error(err.stack); From 8fe3bd288a2fad20470484fe24309e77362575ed Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 22 Mar 2022 21:54:08 -0400 Subject: [PATCH 25/54] chore: release 6.2.8 --- CHANGELOG.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) 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/package.json b/package.json index 1600ad01a04..fb639c25752 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", From 2df2e8b727a8f8b6c7e2ff31fda1b902f32ab538 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 23 Mar 2022 08:41:10 -0400 Subject: [PATCH 26/54] test: add coverage for #11480 --- test/types/middleware.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/types/middleware.test.ts b/test/types/middleware.test.ts index a5903e1c77b..fc98dcd465a 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, expectType, expectNotType } from 'tsd'; interface ITest extends Document { name?: string; @@ -83,4 +83,17 @@ function gh11257(): void { schema.pre('save', { document: true }, function() { expectType>(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 From dc1ede2d7cd33e3c31664668298ef426dd89c847 Mon Sep 17 00:00:00 2001 From: Vandeputte Brice Date: Wed, 23 Mar 2022 13:47:27 +0100 Subject: [PATCH 27/54] date expires option - add unit, behavior, example --- lib/options/SchemaDateOptions.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/options/SchemaDateOptions.js b/lib/options/SchemaDateOptions.js index 09bf27f6adf..85be97c167c 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 takes '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; From 204495c1fdc6c316c86515a888aae6c6507fe743 Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 23 Mar 2022 15:06:22 +0200 Subject: [PATCH 28/54] fix typo --- lib/options/SchemaDateOptions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/options/SchemaDateOptions.js b/lib/options/SchemaDateOptions.js index 85be97c167c..e1a1656d248 100644 --- a/lib/options/SchemaDateOptions.js +++ b/lib/options/SchemaDateOptions.js @@ -48,7 +48,7 @@ Object.defineProperty(SchemaDateOptions.prototype, 'max', opts); /** * If set, Mongoose creates a TTL index on this path. * - * mongo TTL index `expireAfterSeconds` value will takes 'expires' value expressed in seconds. + * mongo TTL index `expireAfterSeconds` value will take 'expires' value expressed in seconds. * * ####Example: * From 5127024afbf020f25095b5e9d95ff0814ada5407 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 23 Mar 2022 10:19:25 -0400 Subject: [PATCH 29/54] perf(document+model): make a few small optimizations re: #11380 --- lib/document.js | 1 + lib/index.js | 3 +- lib/model.js | 118 ++++++++++++++++++++++++------------------------ package.json | 2 +- 4 files changed, 61 insertions(+), 63 deletions(-) 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..ca419a66ef1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -107,8 +107,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..a9312ada2ac 100644 --- a/lib/model.js +++ b/lib/model.js @@ -245,7 +245,6 @@ function _applyCustomWhere(doc, where) { */ Model.prototype.$__handleSave = function(options, callback) { - const _this = this; let saveOptions = {}; applyWriteConcern(this.$__schema, options); @@ -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); + //_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; - } + //this.$__.inserting = true; - const where = this.$__where(delta[0]); - if (where instanceof MongooseError) { - callback(where); - return; - } + return; + } + + // Make sure we don't treat it as a new object on error, + // since it already exists + this.$__.inserting = false; - _applyCustomWhere(this, where); - this[modelCollectionSymbol].updateOne(where, delta[1], saveOptions, (err, ret) => { - if (err) { - this.$__undoReset(); + const delta = this.$__delta(); + if (delta) { + if (delta instanceof MongooseError) { + callback(delta); + return; + } - 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 where = this.$__where(delta[0]); + if (where instanceof MongooseError) { + callback(where); return; } - // store the modified paths before the document is reset - this.$__.modifiedPaths = this.modifiedPaths(); - this.$__reset(); + _applyCustomWhere(this, where); + this[modelCollectionSymbol].updateOne(where, delta[1], saveOptions, (err, ret) => { + if (err) { + this.$__undoReset(); - _setIsNew(this, 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); + 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); }); @@ -516,9 +514,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/package.json b/package.json index fb639c25752..4c7193c1774 100644 --- a/package.json +++ b/package.json @@ -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", From 7f7735c3229c4ccf4ab57ba51076631390c49d50 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 23 Mar 2022 10:30:36 -0400 Subject: [PATCH 30/54] quick fix re: #103800 --- lib/model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/model.js b/lib/model.js index a9312ada2ac..e947710ded3 100644 --- a/lib/model.js +++ b/lib/model.js @@ -305,9 +305,9 @@ Model.prototype.$__handleSave = function(options, callback) { }); this.$__reset(); - //_setIsNew(this, false); + _setIsNew(this, false); // Make it possible to retry the insert - //this.$__.inserting = true; + this.$__.inserting = true; return; } From f4a112b435444fcfc209f82b4f60b8c88e39380c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 23 Mar 2022 10:34:32 -0400 Subject: [PATCH 31/54] style: fix lint re: #11380 --- lib/index.js | 1 - lib/model.js | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/index.js b/lib/index.js index ca419a66ef1..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'); diff --git a/lib/model.js b/lib/model.js index e947710ded3..30413b7a76c 100644 --- a/lib/model.js +++ b/lib/model.js @@ -245,7 +245,7 @@ function _applyCustomWhere(doc, where) { */ Model.prototype.$__handleSave = function(options, callback) { - let saveOptions = {}; + const saveOptions = {}; applyWriteConcern(this.$__schema, options); if (typeof options.writeConcern !== 'undefined') { @@ -311,7 +311,7 @@ Model.prototype.$__handleSave = function(options, callback) { return; } - + // Make sure we don't treat it as a new object on error, // since it already exists this.$__.inserting = false; @@ -421,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); }); From 93dd4929227759191f29b7ebe4bed09d14a674b3 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sun, 6 Mar 2022 02:51:46 +0100 Subject: [PATCH 32/54] add tsafe package to devDependencies --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 4c7193c1774..b5653d5112c 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "pug": "3.0.2", "q": "1.5.1", "serve-handler": "6.1.3", + "tsafe": "^0.9.1", "tsd": "0.19.1", "typescript": "4.5.5", "uuid": "8.3.2", From 2743a05e4d93d218ccb36406d5fce33abc929453 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sun, 6 Mar 2022 02:52:46 +0100 Subject: [PATCH 33/54] create utilities-types helps to infer schema type --- types/infer-doc-type.d.ts | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 types/infer-doc-type.d.ts diff --git a/types/infer-doc-type.d.ts b/types/infer-doc-type.d.ts new file mode 100644 index 00000000000..626e2409302 --- /dev/null +++ b/types/infer-doc-type.d.ts @@ -0,0 +1,41 @@ +import { Schema } from 'mongoose'; + +type RequiredPropertyKeys = { + [K in keyof T]: T[K] extends { required: any } ? K : never; +}[keyof T]; + +type RequiredProperties = { + [K in RequiredPropertyKeys]: T[K]; +}; + +type OptionalPropertyKeys = { + [K in keyof T]: T[K] extends { required: any } ? never : K; +}[keyof T]; + +type OptionalProperties = { + [K in OptionalPropertyKeys]?: T[K]; +}; + +type ResolvePropertyType = PropertyValue extends ( + ...args: any +) => any + ? ReturnType + : PropertyValue; + +export type ObtainDocumentPropertyType = ResolvePropertyType< + PropertyValue extends { type: any } + ? ResolvePropertyType + : PropertyValue +>; + +export type ObtainDocumentType = { + [K in keyof (RequiredProperties & + OptionalProperties)]: ObtainDocumentPropertyType; +}; + +export type InferSchemaType = SchemaType extends Schema< + infer DocType +> + ? ObtainDocumentType + : unknown; + From 4ea70f059c555c4954252aae1c82ebe2e8d6f5a9 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sun, 6 Mar 2022 03:15:25 +0100 Subject: [PATCH 34/54] Make some changes that obtain schema type & create some tests --- test/types/models.test.ts | 20 +++++++++++++++++++- test/types/schema.test.ts | 30 +++++++++++++++++++++++++++++- types/index.d.ts | 7 ++++--- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 3878dbce1a3..7502aad51ce 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 { m0_0aSchema } from './schema.test'; function conventionalSyntax(): void { interface ITest extends Document { @@ -209,4 +210,21 @@ 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' })); + +function m0_0aModel() { + const AutoTypeSchema = m0_0aSchema(); + const AutoTypeModel = model('AutoTypeModel', AutoTypeSchema); + + /* -------------------------------------------------------------------------- */ + /* Model-functions-test */ + /* -------------------------------------------------------------------------- */ + + AutoTypeModel.create({ }); + + /* -------------------------------------------------------------------------- */ + /* Instance-Test */ + /* -------------------------------------------------------------------------- */ + + const AutoTypeModelInstance = new AutoTypeModel({ }); +} diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 09e36e6cc00..d6354756e86 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1,5 +1,7 @@ import { Schema, Document, SchemaDefinition, Model, Types } from 'mongoose'; -import { expectType, expectNotType, expectError } from 'tsd'; +import { expectError } from 'tsd'; +import { assert, Equals } from 'tsafe'; +import { InferSchemaType } from '../../types/infer-doc-type'; enum Genre { Action, @@ -305,3 +307,29 @@ function gh11435(): void { new Schema({}, { expires: '5 seconds' }); expectError(new Schema({}, { expireAfterSeconds: '5 seconds' })); new Schema({}, { expireAfterSeconds: 5 }); + +export type M0_0aAutoTypedSchemaType = { + userName: string; + description?: string; +} + +export function m0_0aSchema() { + const AutoTypedSchema = new Schema({ + userName: { + type: String, + required: [true, 'userName is required'] + }, + description: String + }); + + assert, M0_0aAutoTypedSchemaType >>(); + + // @ts-expect-error: This should complain because isNotExist property not exist in AutoTypeSchema. + assert, { + userName: string; + description?: string; + isNotExist: boolean; + }>>(); + + return AutoTypedSchema; +} diff --git a/types/index.d.ts b/types/index.d.ts index 6a5d4db9cac..8eb78f01c38 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -9,6 +9,7 @@ import events = require('events'); import mongodb = require('mongodb'); import mongoose = require('mongoose'); +import { ObtainDocumentPropertyType, ObtainDocumentType } from './infer-doc-type'; declare module 'mongoose' { @@ -230,7 +231,7 @@ declare module 'mongoose' { discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId): U; } - type AnyKeys = { [P in keyof T]?: T[P] | any }; + type AnyKeys = { [P in keyof T]?: ObtainDocumentPropertyType }; interface AnyObject { [k: string]: any } type Require_id = T extends { _id?: any } ? (T & { _id: T['_id'] }) : (T & { _id: Types.ObjectId }); @@ -252,7 +253,7 @@ declare module 'mongoose' { export const Model: Model; interface Model extends NodeJS.EventEmitter, AcceptsDiscriminator { - new & AnyObject>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; + new & AnyObject>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; aggregate(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback): Aggregate>; aggregate(pipeline: PipelineStage[], cb: Function): Aggregate>; @@ -763,7 +764,7 @@ declare module 'mongoose' { /** * 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; From 8ce5c39804a252b3f6eb03ecf9cd78d1a3a9dfd6 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 12:34:54 +0100 Subject: [PATCH 35/54] Remove tsafe & rewrite related tests --- package.json | 1 - test/types/schema.test.ts | 14 +++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index b5653d5112c..4c7193c1774 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "pug": "3.0.2", "q": "1.5.1", "serve-handler": "6.1.3", - "tsafe": "^0.9.1", "tsd": "0.19.1", "typescript": "4.5.5", "uuid": "8.3.2", diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index d6354756e86..b94d1b5bec7 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1,6 +1,5 @@ import { Schema, Document, SchemaDefinition, Model, Types } from 'mongoose'; -import { expectError } from 'tsd'; -import { assert, Equals } from 'tsafe'; +import { expectError, expectType } from 'tsd'; import { InferSchemaType } from '../../types/infer-doc-type'; enum Genre { @@ -322,14 +321,11 @@ export function m0_0aSchema() { description: String }); - assert, M0_0aAutoTypedSchemaType >>(); + type InferredSchemaType = InferSchemaType - // @ts-expect-error: This should complain because isNotExist property not exist in AutoTypeSchema. - assert, { - userName: string; - description?: string; - isNotExist: boolean; - }>>(); + expectType({} as InferredSchemaType); + + expectError({} as InferredSchemaType); return AutoTypedSchema; } From 85e4c1cb68ba753d8b2f355d23e74649cc25636a Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 12:44:11 +0100 Subject: [PATCH 36/54] Reimplement auto DocType inference --- test/types/models.test.ts | 2 -- types/index.d.ts | 17 ++++++++--------- types/infer-doc-type.d.ts | 15 +++++++-------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 7502aad51ce..995de4500a6 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -220,8 +220,6 @@ function m0_0aModel() { /* Model-functions-test */ /* -------------------------------------------------------------------------- */ - AutoTypeModel.create({ }); - /* -------------------------------------------------------------------------- */ /* Instance-Test */ /* -------------------------------------------------------------------------- */ diff --git a/types/index.d.ts b/types/index.d.ts index 8eb78f01c38..e55b7faad54 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -9,7 +9,7 @@ import events = require('events'); import mongodb = require('mongodb'); import mongoose = require('mongoose'); -import { ObtainDocumentPropertyType, ObtainDocumentType } from './infer-doc-type'; +import { ObtainDocumentType } from './infer-doc-type'; declare module 'mongoose' { @@ -130,13 +130,12 @@ 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?: Schema | Schema, collection?: string, options?: CompileModelOptions - ): U; + ): U extends Model ? Model, TQueryHelpers & ITQ, ITM, ITV> & Omit>: Model, TQueryHelpers>; /** Returns an array of model names created on this instance of Mongoose. */ export function modelNames(): Array; @@ -231,7 +230,7 @@ declare module 'mongoose' { discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId): U; } - type AnyKeys = { [P in keyof T]?: ObtainDocumentPropertyType }; + type AnyKeys = { [P in keyof T]?: T[P] | any}; interface AnyObject { [k: string]: any } type Require_id = T extends { _id?: any } ? (T & { _id: T['_id'] }) : (T & { _id: Types.ObjectId }); @@ -253,7 +252,7 @@ declare module 'mongoose' { export const Model: Model; interface Model extends NodeJS.EventEmitter, AcceptsDiscriminator { - new & AnyObject>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; + new & AnyObject>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; aggregate(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback): Aggregate>; aggregate(pipeline: PipelineStage[], cb: Function): Aggregate>; @@ -760,11 +759,11 @@ 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 = any, TQueryHelpers = any, DocDefinition = unknown> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); + constructor(definition?: SchemaDefinition> | DocDefinition, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; diff --git a/types/infer-doc-type.d.ts b/types/infer-doc-type.d.ts index 626e2409302..933978efc54 100644 --- a/types/infer-doc-type.d.ts +++ b/types/infer-doc-type.d.ts @@ -28,14 +28,13 @@ export type ObtainDocumentPropertyType = ResolvePropertyType< : PropertyValue >; -export type ObtainDocumentType = { - [K in keyof (RequiredProperties & - OptionalProperties)]: ObtainDocumentPropertyType; +export type ObtainDocumentType = DoesDocTypeExist extends true ? DocType :{ + [K in keyof (RequiredProperties & + OptionalProperties)]: ObtainDocumentPropertyType; }; -export type InferSchemaType = SchemaType extends Schema< - infer DocType -> - ? ObtainDocumentType - : unknown; +export type InferSchemaType = SchemaType extends Schema + ? DoesDocTypeExist extends true ? DocType : ObtainDocumentType + : unknown; +export type DoesDocTypeExist = keyof DocType extends string ? true : false; From 6961e1cf74fd982cdddba284050bf6172cfe2533 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 13:18:08 +0100 Subject: [PATCH 37/54] Make Model instance show correct type instead of "any" --- test/types/models.test.ts | 10 +++++++--- types/index.d.ts | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 995de4500a6..09405848d9c 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -1,6 +1,6 @@ import { Schema, Document, Model, Types, connection, model } from 'mongoose'; -import { expectError } from 'tsd'; -import { m0_0aSchema } from './schema.test'; +import { expectError, expectType } from 'tsd'; +import { M0_0aAutoTypedSchemaType, m0_0aSchema } from './schema.test'; function conventionalSyntax(): void { interface ITest extends Document { @@ -220,9 +220,13 @@ function m0_0aModel() { /* Model-functions-test */ /* -------------------------------------------------------------------------- */ + AutoTypeModel.create({ }); + /* -------------------------------------------------------------------------- */ /* Instance-Test */ /* -------------------------------------------------------------------------- */ - const AutoTypeModelInstance = new AutoTypeModel({ }); + const AutoTypeModelInstance = new AutoTypeModel({}); + + expectType(AutoTypeModelInstance.userName); } diff --git a/types/index.d.ts b/types/index.d.ts index e55b7faad54..ac4d61e9add 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -252,7 +252,7 @@ declare module 'mongoose' { export const Model: Model; interface Model extends NodeJS.EventEmitter, AcceptsDiscriminator { - new & AnyObject>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; + 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>; From 0795b69ca65abd9e2437e05dfebb8bccb22b339e Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 14:33:39 +0100 Subject: [PATCH 38/54] Refact ApplyBasicQueryCasting type to make schema properties types visible --- test/types/models.test.ts | 2 +- types/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 09405848d9c..9ed7946d115 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -220,7 +220,7 @@ function m0_0aModel() { /* Model-functions-test */ /* -------------------------------------------------------------------------- */ - AutoTypeModel.create({ }); + AutoTypeModel.find({ }); /* -------------------------------------------------------------------------- */ /* Instance-Test */ diff --git a/types/index.d.ts b/types/index.d.ts index ac4d61e9add..3368f600fa1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1979,7 +1979,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 = { From 40cf9a09badcd2cb0f3de2b6bb615cc28f3d565c Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 15:46:13 +0100 Subject: [PATCH 39/54] Make schema props type visible --- test/types/models.test.ts | 1 + test/types/schema.test.ts | 16 +++++++++++++++- types/index.d.ts | 20 ++++++++++---------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 9ed7946d115..2a29a32d326 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -221,6 +221,7 @@ function m0_0aModel() { /* -------------------------------------------------------------------------- */ AutoTypeModel.find({ }); + AutoTypeModel.create({}); /* -------------------------------------------------------------------------- */ /* Instance-Test */ diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index b94d1b5bec7..6ce01080953 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -310,6 +310,10 @@ new Schema({}, { expireAfterSeconds: 5 }); export type M0_0aAutoTypedSchemaType = { userName: string; description?: string; + nested?: { + age: number; + hobby?: string + } } export function m0_0aSchema() { @@ -318,7 +322,17 @@ export function m0_0aSchema() { type: String, required: [true, 'userName is required'] }, - description: String + description: String, + nested: new Schema({ + age: { + type: Number, + required: true + }, + hobby: { + type: String, + required: false + } + }) }); type InferredSchemaType = InferSchemaType diff --git a/types/index.d.ts b/types/index.d.ts index 3368f600fa1..c328268535d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -295,15 +295,15 @@ 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: (T)[], options?: SaveOptions): Promise[]>; + create(docs: (T)[], callback: Callback[]>): void; + create(doc: T): Promise>; + create(doc: T, 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 the collection for this model. By default, if no indexes are specified, @@ -2048,7 +2048,7 @@ declare module 'mongoose' { [Extract] extends [never] ? T : T | string; type _UpdateQueryDef = { - [K in keyof T]?: __UpdateDefProperty; + [K in keyof T]?: __UpdateDefProperty | any; }; /** From 2139bce1891169518c3207bcf79aac969e181ceb Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 15:46:39 +0100 Subject: [PATCH 40/54] Make InferredSchemaType support nested schemas --- types/infer-doc-type.d.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/types/infer-doc-type.d.ts b/types/infer-doc-type.d.ts index 933978efc54..cf617553b67 100644 --- a/types/infer-doc-type.d.ts +++ b/types/infer-doc-type.d.ts @@ -1,7 +1,7 @@ import { Schema } from 'mongoose'; type RequiredPropertyKeys = { - [K in keyof T]: T[K] extends { required: any } ? K : never; + [K in keyof T]: T[K] extends { required: true | [true, string|undefined] } ? K : never; }[keyof T]; type RequiredProperties = { @@ -9,7 +9,7 @@ type RequiredProperties = { }; type OptionalPropertyKeys = { - [K in keyof T]: T[K] extends { required: any } ? never : K; + [K in keyof T]: T[K] extends { required: true | [true, string|undefined] } ? never : K; }[keyof T]; type OptionalProperties = { @@ -22,13 +22,14 @@ type ResolvePropertyType = PropertyValue extends ( ? ReturnType : PropertyValue; -export type ObtainDocumentPropertyType = ResolvePropertyType< +export type ObtainDocumentPropertyType = PropertyValue extends Schema ? InferSchemaType: ResolvePropertyType< PropertyValue extends { type: any } ? ResolvePropertyType : PropertyValue >; -export type ObtainDocumentType = DoesDocTypeExist extends true ? DocType :{ +export type ObtainDocumentType = + DoesDocTypeExist extends true ? DocType : { [K in keyof (RequiredProperties & OptionalProperties)]: ObtainDocumentPropertyType; }; From 622071ba9f9ea138e44527cfc1a717d7b7ea876d Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Mon, 7 Mar 2022 17:11:15 +0100 Subject: [PATCH 41/54] Make statics functions auto typed by supplying them to schema options --- lib/schema.js | 2 +- test/model.test.js | 10 +++++++++- test/types/models.test.ts | 11 +++++++++-- test/types/schema.test.ts | 11 ++++++++--- types/index.d.ts | 14 +++++++------- 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index f5a08244602..df6a49a3d0e 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -108,7 +108,7 @@ function Schema(obj, options) { this._indexes = []; this.methods = {}; this.methodOptions = {}; - this.statics = {}; + this.statics = options?.statics || {}; this.tree = {}; this.query = {}; this.childSchemas = []; diff --git a/test/model.test.js b/test/model.test.js index 2cfce80e7b5..46486df1b71 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', function() { + it('should give an array back rather than undefined m0_0aAutoTyped', 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/models.test.ts b/test/types/models.test.ts index 2a29a32d326..67362d12cd8 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -212,7 +212,7 @@ Project.createCollection({ expires: '5 seconds' }); Project.createCollection({ expireAfterSeconds: 5 }); expectError(Project.createCollection({ expireAfterSeconds: '5 seconds' })); -function m0_0aModel() { +export function m0_0aModel() { const AutoTypeSchema = m0_0aSchema(); const AutoTypeModel = model('AutoTypeModel', AutoTypeSchema); @@ -223,11 +223,18 @@ function m0_0aModel() { AutoTypeModel.find({ }); AutoTypeModel.create({}); + /* -------------------------------------------------------------------------- */ + /* Model-statics-functions-test */ + /* -------------------------------------------------------------------------- */ + expectType>(AutoTypeModel.staticFn()); + /* -------------------------------------------------------------------------- */ /* Instance-Test */ /* -------------------------------------------------------------------------- */ const AutoTypeModelInstance = new AutoTypeModel({}); - expectType(AutoTypeModelInstance.userName); + expectType(AutoTypeModelInstance.userName); + + return AutoTypeModel; } diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 6ce01080953..872f789c5ac 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -308,12 +308,17 @@ expectError(new Schema({}, { expireAfterSeconds: '5 seconds' })); new Schema({}, { expireAfterSeconds: 5 }); export type M0_0aAutoTypedSchemaType = { + schema: { userName: string; description?: string; nested?: { age: number; hobby?: string } + } + , statics: { + staticFn:()=>'Returned from staticFn' + } } export function m0_0aSchema() { @@ -333,13 +338,13 @@ export function m0_0aSchema() { required: false } }) - }); + }, { statics: { staticFn() {return 'Returned from staticFn';} } }); type InferredSchemaType = InferSchemaType - expectType({} as InferredSchemaType); + expectType({} as InferredSchemaType); - expectError({} as InferredSchemaType); + expectError({} as InferredSchemaType); return AutoTypedSchema; } diff --git a/types/index.d.ts b/types/index.d.ts index c328268535d..862422b6200 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -130,12 +130,12 @@ declare module 'mongoose' { */ export function isObjectIdOrHexString(v: any): boolean; - export function model( + export function model( name: string, - schema?: Schema | Schema, + schema?: Schema | Schema, collection?: string, options?: CompileModelOptions - ): U extends Model ? Model, TQueryHelpers & ITQ, ITM, ITV> & Omit>: Model, TQueryHelpers>; + ): U extends Model ? Model, TQueryHelpers & ITQ, ITM, ITV, StaticsMethods> & Omit> : Model, TQueryHelpers, {}, {}, StaticsMethods>; /** Returns an array of model names created on this instance of Mongoose. */ export function modelNames(): Array; @@ -251,8 +251,8 @@ declare module 'mongoose' { } export const Model: Model; - interface Model extends NodeJS.EventEmitter, AcceptsDiscriminator { - new(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; + type Model = NodeJS.EventEmitter& AcceptsDiscriminator & StaticsMethods &{ + 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>; @@ -759,11 +759,11 @@ 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 = any, TQueryHelpers = any, DocDefinition = unknown> extends events.EventEmitter { + class Schema, TInstanceMethods = any, TQueryHelpers = any, DocDefinition = unknown, StaticsMethods = {}> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocDefinition, options?: SchemaOptions); + constructor(definition?: SchemaDefinition> | DocDefinition, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; From 3c539a9f2bfe43f676afaf1676745b613bf5fdd4 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Wed, 9 Mar 2022 14:39:05 +0100 Subject: [PATCH 42/54] Refact Schema & Model types --- types/index.d.ts | 16 ++++++++++------ types/infer-doc-type.d.ts | 10 ++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 862422b6200..c049e59d88a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -9,7 +9,7 @@ import events = require('events'); import mongodb = require('mongodb'); import mongoose = require('mongoose'); -import { ObtainDocumentType } from './infer-doc-type'; +import { InferSchemaType, ObtainDocumentType, ObtainSchemaGeneric } from './infer-doc-type'; declare module 'mongoose' { @@ -130,12 +130,14 @@ declare module 'mongoose' { */ export function isObjectIdOrHexString(v: any): boolean; - export function model( +export function model( name: string, - schema?: Schema | Schema, + schema?: TSchema, collection?: string, options?: CompileModelOptions - ): U extends Model ? Model, TQueryHelpers & ITQ, ITM, ITV, StaticsMethods> & Omit> : Model, TQueryHelpers, {}, {}, StaticsMethods>; +): U extends Model + ? U + : Model, TQueryHelpers, {}, {}, TSchema>; /** Returns an array of model names created on this instance of Mongoose. */ export function modelNames(): Array; @@ -251,7 +253,9 @@ declare module 'mongoose' { } export const Model: Model; - type Model = NodeJS.EventEmitter& AcceptsDiscriminator & StaticsMethods &{ + type Model = NodeJS.EventEmitter& AcceptsDiscriminator & ObtainSchemaGeneric &{ + + // type Model = NodeJS.EventEmitter& AcceptsDiscriminator & StaticsMethods &{ new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; aggregate(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback): Aggregate>; @@ -759,7 +763,7 @@ 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 = any, TQueryHelpers = any, DocDefinition = unknown, StaticsMethods = {}> extends events.EventEmitter { + class Schema, TInstanceMethods = any, TQueryHelpers = any, DocDefinition extends ObtainDocumentType = any, StaticsMethods = {}> extends events.EventEmitter { /** * Create a new schema */ diff --git a/types/infer-doc-type.d.ts b/types/infer-doc-type.d.ts index cf617553b67..8e83e04c0da 100644 --- a/types/infer-doc-type.d.ts +++ b/types/infer-doc-type.d.ts @@ -22,11 +22,13 @@ type ResolvePropertyType = PropertyValue extends ( ? ReturnType : PropertyValue; -export type ObtainDocumentPropertyType = PropertyValue extends Schema ? InferSchemaType: ResolvePropertyType< +type ObtainDocumentPropertyType = PropertyValue extends Schema ? InferSchemaType: ResolvePropertyType< PropertyValue extends { type: any } ? ResolvePropertyType : PropertyValue ->; + >; + +type DoesDocTypeExist = keyof DocType extends string ? true : false; export type ObtainDocumentType = DoesDocTypeExist extends true ? DocType : { @@ -35,7 +37,7 @@ export type ObtainDocumentType = }; export type InferSchemaType = SchemaType extends Schema - ? DoesDocTypeExist extends true ? DocType : ObtainDocumentType + ? DoesDocTypeExist extends true ? DocType : DocDefinition : unknown; -export type DoesDocTypeExist = keyof DocType extends string ? true : false; +export type ObtainSchemaGeneric = TSchema extends Schema ? {DocType:DocType, M:M, TInstanceMethods:TInstanceMethods, TQueryHelpers:TQueryHelpers, DocDefinition:DocDefinition, StaticsMethods:StaticsMethods}[name] : never; \ No newline at end of file From 3fc1cb75ad40c7c1d8ec9650e319b0010601f61b Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Wed, 9 Mar 2022 17:28:11 +0100 Subject: [PATCH 43/54] Refact some infer-doc-type import lines --- lib/schema.js | 2 +- test/model.test.js | 4 ++-- test/types/schema.test.ts | 3 +-- types/index.d.ts | 2 +- types/infer-doc-type.d.ts | 43 --------------------------------- types/inferschematype.d.ts | 49 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 54 insertions(+), 49 deletions(-) delete mode 100644 types/infer-doc-type.d.ts create mode 100644 types/inferschematype.d.ts diff --git a/lib/schema.js b/lib/schema.js index df6a49a3d0e..8dd8bad847d 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -108,7 +108,7 @@ function Schema(obj, options) { this._indexes = []; this.methods = {}; this.methodOptions = {}; - this.statics = options?.statics || {}; + this.statics = (options && options.statics) || {}; this.tree = {}; this.query = {}; this.childSchemas = []; diff --git a/test/model.test.js b/test/model.test.js index 46486df1b71..ecac4212450 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -8487,8 +8487,8 @@ function pick(obj, keys) { return newObj; } -describe('Check if statics functions that is supplied in schema option is availabe', function() { - it('should give an array back rather than undefined m0_0aAutoTyped', function M0_0aModelJS() { +describe('Check if statics functions that is supplied in schema option is availabe (m0_0a)', function() { + it('should give an 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'); diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 872f789c5ac..0eb65804fe1 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1,6 +1,5 @@ -import { Schema, Document, SchemaDefinition, Model, Types } from 'mongoose'; +import { Schema, Document, SchemaDefinition, Model, Types, InferSchemaType } from 'mongoose'; import { expectError, expectType } from 'tsd'; -import { InferSchemaType } from '../../types/infer-doc-type'; enum Genre { Action, diff --git a/types/index.d.ts b/types/index.d.ts index c049e59d88a..e24b3f54d25 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -5,11 +5,11 @@ /// /// /// +/// import events = require('events'); import mongodb = require('mongodb'); import mongoose = require('mongoose'); -import { InferSchemaType, ObtainDocumentType, ObtainSchemaGeneric } from './infer-doc-type'; declare module 'mongoose' { diff --git a/types/infer-doc-type.d.ts b/types/infer-doc-type.d.ts deleted file mode 100644 index 8e83e04c0da..00000000000 --- a/types/infer-doc-type.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Schema } from 'mongoose'; - -type RequiredPropertyKeys = { - [K in keyof T]: T[K] extends { required: true | [true, string|undefined] } ? K : never; -}[keyof T]; - -type RequiredProperties = { - [K in RequiredPropertyKeys]: T[K]; -}; - -type OptionalPropertyKeys = { - [K in keyof T]: T[K] extends { required: true | [true, string|undefined] } ? never : K; -}[keyof T]; - -type OptionalProperties = { - [K in OptionalPropertyKeys]?: T[K]; -}; - -type ResolvePropertyType = PropertyValue extends ( - ...args: any -) => any - ? ReturnType - : PropertyValue; - -type ObtainDocumentPropertyType = PropertyValue extends Schema ? InferSchemaType: ResolvePropertyType< - PropertyValue extends { type: any } - ? ResolvePropertyType - : PropertyValue - >; - -type DoesDocTypeExist = keyof DocType extends string ? true : false; - -export type ObtainDocumentType = - DoesDocTypeExist extends true ? DocType : { - [K in keyof (RequiredProperties & - OptionalProperties)]: ObtainDocumentPropertyType; -}; - -export type InferSchemaType = SchemaType extends Schema - ? DoesDocTypeExist extends true ? DocType : DocDefinition - : unknown; - -export type ObtainSchemaGeneric = TSchema extends Schema ? {DocType:DocType, M:M, TInstanceMethods:TInstanceMethods, TQueryHelpers:TQueryHelpers, DocDefinition:DocDefinition, StaticsMethods:StaticsMethods}[name] : never; \ No newline at end of file diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts new file mode 100644 index 00000000000..0212af8a369 --- /dev/null +++ b/types/inferschematype.d.ts @@ -0,0 +1,49 @@ +import { Schema, InferSchemaType } from 'mongoose'; + +declare module 'mongoose' { + type ObtainDocumentType = + DoesDocTypeExist extends true ? DocType : { + [K in keyof (RequiredProperties & + OptionalProperties)]: ObtainDocumentPropertyType; + }; + + type InferSchemaType = SchemaType extends Schema + ? DoesDocTypeExist extends true ? DocType : DocDefinition + : unknown; + + type ObtainSchemaGeneric = + TSchema extends Schema + ? { DocType: DocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, DocDefinition: DocDefinition, StaticsMethods: StaticsMethods }[name] + : never; +} + +type RequiredPropertyKeys = { + [K in keyof T]: T[K] extends { required: true | [true, string | undefined] } ? K : never; +}[keyof T]; + +type RequiredProperties = { + [K in RequiredPropertyKeys]: T[K]; +}; + +type OptionalPropertyKeys = { + [K in keyof T]: T[K] extends { required: true | [true, string | undefined] } ? never : K; +}[keyof T]; + +type OptionalProperties = { + [K in OptionalPropertyKeys]?: T[K]; +}; + +type ResolvePropertyType = PropertyValue extends ( + ...args: any +) => any + ? ReturnType + : PropertyValue; + +type ObtainDocumentPropertyType = PropertyValue extends Schema + ? InferSchemaType + : ResolvePropertyType + : PropertyValue +>; + +type DoesDocTypeExist = keyof DocType extends string ? true : false; \ No newline at end of file From f1c0a9c666532d1c0ec24b334a80aabbb6373d70 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Thu, 10 Mar 2022 16:50:47 +0100 Subject: [PATCH 44/54] Create some tests for Model functions --- test/types/models.test.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 67362d12cd8..7a54618a8e3 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -212,20 +212,30 @@ Project.createCollection({ expires: '5 seconds' }); Project.createCollection({ expireAfterSeconds: 5 }); expectError(Project.createCollection({ expireAfterSeconds: '5 seconds' })); -export function m0_0aModel() { +export async function m0_0aModel() { const AutoTypeSchema = m0_0aSchema(); const AutoTypeModel = model('AutoTypeModel', AutoTypeSchema); /* -------------------------------------------------------------------------- */ /* Model-functions-test */ /* -------------------------------------------------------------------------- */ + // TODO: check if these tests work after merging this PR: https://github.com/Automattic/mongoose/pull/11503 - AutoTypeModel.find({ }); - AutoTypeModel.create({}); + /* + const testDoc1 = await AutoTypeModel.create({ userName: 'M0_0a' }); + expectType(testDoc.userName); + expectType(testDoc.nested.age); + + const testDoc2 = await AutoTypeModel.findOne({ userName: 'M0_0a' }); + expectType(testDoc2.userName); + expectType(testDoc2.nested.age); + + */ /* -------------------------------------------------------------------------- */ /* Model-statics-functions-test */ /* -------------------------------------------------------------------------- */ + expectType>(AutoTypeModel.staticFn()); /* -------------------------------------------------------------------------- */ From b4c5e0a8e073d9cf4718be6ee9ff24094c818582 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Thu, 10 Mar 2022 17:37:51 +0100 Subject: [PATCH 45/54] Infer document instance methods from methods object supplied in schema options --- lib/schema.js | 2 +- test/document.test.js | 8 ++++++++ test/types/models.test.ts | 7 +++++++ test/types/schema.test.ts | 12 +++++++++++- types/index.d.ts | 14 ++++++++------ types/schemaoptions.d.ts | 14 ++++++++++++-- 6 files changed, 47 insertions(+), 10 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index 8dd8bad847d..ba7a840df3b 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -106,7 +106,7 @@ function Schema(obj, options) { this.inherits = {}; this.callQueue = []; this._indexes = []; - this.methods = {}; + this.methods = (options && options.methods) || {}; this.methodOptions = {}; this.statics = (options && options.statics) || {}; this.tree = {}; diff --git a/test/document.test.js b/test/document.test.js index 5d630a7e334..3eb8943585c 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', 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/types/models.test.ts b/test/types/models.test.ts index 7a54618a8e3..90740708f77 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -246,5 +246,12 @@ export async function m0_0aModel() { expectType(AutoTypeModelInstance.userName); + /* -------------------------------------------------------------------------- */ + /* Document-Instance-Methods */ + /* -------------------------------------------------------------------------- */ + + expectType>(AutoTypeModelInstance.instanceFn()); + + return AutoTypeModel; } diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 0eb65804fe1..1f698100fdb 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -317,6 +317,9 @@ export type M0_0aAutoTypedSchemaType = { } , statics: { staticFn:()=>'Returned from staticFn' + }, + methods: { + instanceFn : ()=> 'Returned from DocumentInstanceFn' } } @@ -337,7 +340,14 @@ export function m0_0aSchema() { required: false } }) - }, { statics: { staticFn() {return 'Returned from staticFn';} } }); + }, { + statics: { + staticFn() { return 'Returned from staticFn'; } + }, + methods: { + instanceFn() { return 'Returned from DocumentInstanceFn'; } + } + }); type InferredSchemaType = InferSchemaType diff --git a/types/index.d.ts b/types/index.d.ts index e24b3f54d25..d92b171b56a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -137,7 +137,7 @@ export function model( options?: CompileModelOptions ): U extends Model ? U - : Model, TQueryHelpers, {}, {}, TSchema>; + : Model, TQueryHelpers, ObtainSchemaGeneric, {}, TSchema>; /** Returns an array of model names created on this instance of Mongoose. */ export function modelNames(): Array; @@ -253,10 +253,9 @@ export function model( } export const Model: Model; - type Model = NodeJS.EventEmitter& AcceptsDiscriminator & ObtainSchemaGeneric &{ + type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { - // type Model = NodeJS.EventEmitter& AcceptsDiscriminator & StaticsMethods &{ - new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; + new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument & */ TMethodsAndOverrides, TVirtuals>; aggregate(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback): Aggregate>; aggregate(pipeline: PipelineStage[], cb: Function): Aggregate>; @@ -763,11 +762,14 @@ export function model( 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 = any, TQueryHelpers = any, DocDefinition extends ObtainDocumentType = any, StaticsMethods = {}> extends events.EventEmitter { + class Schema, TInstanceMethods = {}, TQueryHelpers = any, + DocDefinition extends ObtainDocumentType = any, + StaticsMethods = {}> + extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocDefinition, options?: SchemaOptions); + constructor(definition?: SchemaDefinition> | DocDefinition, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 64edcbd2608..10ab7d8ab3c 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -8,7 +8,7 @@ declare module 'mongoose' { currentTime?: () => (NativeDate | number); } - interface SchemaOptions { + 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 @@ -184,6 +184,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?: StaticsMethods, + + /** + * Document instance methods. + */ + methods?: InstanceMethods, } } \ No newline at end of file From c9aec3a143166c7fe2ca22f5463d75f0220de6c5 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Thu, 10 Mar 2022 19:10:37 +0100 Subject: [PATCH 46/54] Use auto infered docType as default type every where --- test/types/middleware.test.ts | 4 ++-- types/index.d.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/types/middleware.test.ts b/test/types/middleware.test.ts index fc98dcd465a..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, expectNotType } from 'tsd'; +import { expectError, expectAssignable, expectNotType } from 'tsd'; interface ITest extends Document { name?: string; @@ -81,7 +81,7 @@ const Test = model('Test', schema); function gh11257(): void { schema.pre('save', { document: true }, function() { - expectType>(this); + expectAssignable>(this); }); } diff --git a/types/index.d.ts b/types/index.d.ts index d92b171b56a..64a1ac87355 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -255,7 +255,7 @@ export function model( export const Model: Model; type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { - new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument & */ TMethodsAndOverrides, TVirtuals>; + 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>; @@ -762,17 +762,17 @@ export function model( 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 = any, - DocDefinition extends ObtainDocumentType = any, + class Schema, TInstanceMethods = {}, TQueryHelpers = any, + DocType extends ObtainDocumentType = any, StaticsMethods = {}> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocDefinition, 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) @@ -822,7 +822,7 @@ export function model( 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; From 818bac06021962d2b0ce047e414d53e70746e7dd Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Fri, 11 Mar 2022 16:15:16 +0100 Subject: [PATCH 47/54] Improve ResolvePathType ability to resolve correct type of types string aliases like "String" | "Number" --- test/types/schema.test.ts | 90 +++++++++++++++++++++++++++++++++++++- types/inferschematype.d.ts | 62 +++++++++++++++----------- 2 files changed, 126 insertions(+), 26 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 1f698100fdb..f42c5c50d40 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1,4 +1,4 @@ -import { Schema, Document, SchemaDefinition, Model, Types, InferSchemaType } from 'mongoose'; +import { Schema, Document, SchemaDefinition, Model, Types, InferSchemaType, SchemaType } from 'mongoose'; import { expectError, expectType } from 'tsd'; enum Genre { @@ -355,5 +355,93 @@ export function m0_0aSchema() { 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); + return AutoTypedSchema; } diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 0212af8a369..e3d244f5652 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -1,10 +1,10 @@ -import { Schema, InferSchemaType } from 'mongoose'; +import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions } from 'mongoose'; declare module 'mongoose' { type ObtainDocumentType = DoesDocTypeExist extends true ? DocType : { - [K in keyof (RequiredProperties & - OptionalProperties)]: ObtainDocumentPropertyType; + [K in keyof (RequiredPaths & + OptionalPaths)]: ObtainDocumentPathType; }; type InferSchemaType = SchemaType extends Schema @@ -17,33 +17,45 @@ declare module 'mongoose' { : never; } -type RequiredPropertyKeys = { - [K in keyof T]: T[K] extends { required: true | [true, string | undefined] } ? K : never; -}[keyof T]; +type DoesDocTypeExist = keyof DocType extends string ? true : false; -type RequiredProperties = { - [K in RequiredPropertyKeys]: T[K]; -}; +type RequiredPathBaseType = { required: true | [true, string | undefined] } + +type PathWithTypePropertyBaseType = { type: any } -type OptionalPropertyKeys = { - [K in keyof T]: T[K] extends { required: true | [true, string | undefined] } ? never : K; +type RequiredPathKeys = { + [K in keyof T]: T[K] extends RequiredPathBaseType ? K : never; }[keyof T]; -type OptionalProperties = { - [K in OptionalPropertyKeys]?: T[K]; +type RequiredPaths = { + [K in RequiredPathKeys]: T[K]; }; -type ResolvePropertyType = PropertyValue extends ( - ...args: any -) => any - ? ReturnType - : PropertyValue; +type OptionalPathKeys = { + [K in keyof T]: T[K] extends RequiredPathBaseType ? never : K; +}[keyof T]; -type ObtainDocumentPropertyType = PropertyValue extends Schema - ? InferSchemaType - : ResolvePropertyType - : PropertyValue ->; +type OptionalPaths = { + [K in OptionalPathKeys]?: T[K]; +}; -type DoesDocTypeExist = keyof DocType extends string ? true : false; \ No newline at end of file +type ObtainDocumentPathType = PathValueType extends Schema + ? InferSchemaType + : ResolvePathType< + PathValueType extends PathWithTypePropertyBaseType ? PathValueType['type'] : PathValueType, + PathValueType extends PathWithTypePropertyBaseType ? Omit : {} + >; + +type ResolvePathType = {}> = +PathValueType extends (infer Item)[] ? ResolvePathType[] : +PathValueType extends StringConstructor | 'string' | 'String' | typeof Schema.Types.String ? string : +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'] : +PathValueType \ No newline at end of file From 239846ee95f41cce0e73b19af25ca916a08fbdcb Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sat, 12 Mar 2022 02:28:52 +0100 Subject: [PATCH 48/54] create & implement FlexibleObject & refact typos --- test/model.test.js | 2 +- test/types/models.test.ts | 5 +++-- types/index.d.ts | 23 ++++++++++------------- types/inferschematype.d.ts | 11 ++++++----- types/schemaoptions.d.ts | 4 ++-- 5 files changed, 22 insertions(+), 23 deletions(-) diff --git a/test/model.test.js b/test/model.test.js index ecac4212450..517e3f8a3a7 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -8488,7 +8488,7 @@ function pick(obj, keys) { } describe('Check if statics functions that is supplied in schema option is availabe (m0_0a)', function() { - it('should give an static function back rather than undefined', function M0_0aModelJS() { + 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'); diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 90740708f77..ec56c9ffd47 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -219,8 +219,10 @@ export async function m0_0aModel() { /* -------------------------------------------------------------------------- */ /* Model-functions-test */ /* -------------------------------------------------------------------------- */ - // TODO: check if these tests work after merging this PR: https://github.com/Automattic/mongoose/pull/11503 + // Create should works with arbitrary objects. + const testDoc1 = await AutoTypeModel.create({ unExistKey: 'unExistKey' }); + // TODO: check if these tests work after merging this PR: https://github.com/Automattic/mongoose/pull/11503 /* const testDoc1 = await AutoTypeModel.create({ userName: 'M0_0a' }); expectType(testDoc.userName); @@ -229,7 +231,6 @@ export async function m0_0aModel() { const testDoc2 = await AutoTypeModel.findOne({ userName: 'M0_0a' }); expectType(testDoc2.userName); expectType(testDoc2.nested.age); - */ /* -------------------------------------------------------------------------- */ diff --git a/types/index.d.ts b/types/index.d.ts index 64a1ac87355..1c369c6c9bd 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -232,8 +232,9 @@ export function model( discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId): U; } - type AnyKeys = { [P in keyof T]?: T[P] | any}; + type AnyKeys = { [P in keyof T]?: 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 }); @@ -253,7 +254,7 @@ export function model( } export const Model: Model; - type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { + type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; @@ -298,15 +299,11 @@ export function model( countDocuments(filter: FilterQuery, options?: QueryOptions, callback?: Callback): QueryWithHelpers, TQueryHelpers, T>; /** Creates a new document or documents */ - create(docs: (T)[], options?: SaveOptions): Promise[]>; - create(docs: (T)[], callback: Callback[]>): void; - create(doc: T): Promise>; - create(doc: T, 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: 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 the collection for this model. By default, if no indexes are specified, @@ -764,12 +761,12 @@ export function model( class Schema, TInstanceMethods = {}, TQueryHelpers = any, DocType extends ObtainDocumentType = any, - StaticsMethods = {}> + StaticMethods = {}> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); + constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index e3d244f5652..88ad764fc3d 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -1,8 +1,9 @@ import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions } from 'mongoose'; declare module 'mongoose' { - type ObtainDocumentType = - DoesDocTypeExist extends true ? DocType : { + + type ObtainDocumentType = + DoesDocTypeExist extends true ? EnforcedDocType : { [K in keyof (RequiredPaths & OptionalPaths)]: ObtainDocumentPathType; }; @@ -11,9 +12,9 @@ declare module 'mongoose' { ? DoesDocTypeExist extends true ? DocType : DocDefinition : unknown; - type ObtainSchemaGeneric = - TSchema extends Schema - ? { DocType: DocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, DocDefinition: DocDefinition, StaticsMethods: StaticsMethods }[name] + type ObtainSchemaGeneric = + TSchema extends Schema + ? { DocType: DocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, DocDefinition: DocDefinition, StaticMethods: StaticMethods }[name] : never; } diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 10ab7d8ab3c..91a34e2d5b5 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -8,7 +8,7 @@ declare module 'mongoose' { currentTime?: () => (NativeDate | number); } - interface SchemaOptions { + 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 @@ -189,7 +189,7 @@ declare module 'mongoose' { /** * Model Statics methods. */ - statics?: StaticsMethods, + statics?: StaticMethods, /** * Document instance methods. From 3696661bbe31fea0145e0002353c59b9dbc525e8 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sat, 12 Mar 2022 17:43:58 +0100 Subject: [PATCH 49/54] Delete AnyKeys type & refact create & insertMany types --- test/types/create.test.ts | 2 +- test/types/models.test.ts | 17 ++++++------- types/index.d.ts | 51 +++++++++++++++++++-------------------- 3 files changed, 34 insertions(+), 36 deletions(-) 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/models.test.ts b/test/types/models.test.ts index ec56c9ffd47..33e03eaf1e9 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -1,5 +1,5 @@ import { Schema, Document, Model, Types, connection, model } from 'mongoose'; -import { expectError, expectType } from 'tsd'; +import { expectAssignable, expectError, expectType } from 'tsd'; import { M0_0aAutoTypedSchemaType, m0_0aSchema } from './schema.test'; function conventionalSyntax(): void { @@ -220,18 +220,17 @@ export async function m0_0aModel() { /* Model-functions-test */ /* -------------------------------------------------------------------------- */ // Create should works with arbitrary objects. - const testDoc1 = await AutoTypeModel.create({ unExistKey: 'unExistKey' }); + const randomObject = await AutoTypeModel.create({ unExistKey: 'unExistKey', description: 'st' }); + expectType(randomObject.unExistKey); + expectType(randomObject.userName); - // TODO: check if these tests work after merging this PR: https://github.com/Automattic/mongoose/pull/11503 - /* const testDoc1 = await AutoTypeModel.create({ userName: 'M0_0a' }); - expectType(testDoc.userName); - expectType(testDoc.nested.age); + expectType(testDoc1.userName); + expectType(testDoc1.description); const testDoc2 = await AutoTypeModel.findOne({ userName: 'M0_0a' }); - expectType(testDoc2.userName); - expectType(testDoc2.nested.age); - */ + expectType(testDoc2?.userName); + expectType(testDoc2?.description); /* -------------------------------------------------------------------------- */ /* Model-statics-functions-test */ diff --git a/types/index.d.ts b/types/index.d.ts index 1c369c6c9bd..8059e9b985a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -232,7 +232,6 @@ export function model( discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId): U; } - type AnyKeys = { [P in keyof T]?: any}; interface AnyObject { [k: string]: any } type FlexibleObject = { [P in keyof (T & Omit)]?: P extends keyof T ? T[P] : any } @@ -299,11 +298,11 @@ export function model( countDocuments(filter: FilterQuery, options?: QueryOptions, callback?: Callback): QueryWithHelpers, TQueryHelpers, T>; /** Creates a new document or documents */ - 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 @@ export function model( 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 @@ -1437,7 +1436,7 @@ export function model( /** 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 { @@ -2016,22 +2015,22 @@ export function model( 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?: { From 86558a52956d87b61fe34b14d339b318cd804a3a Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sun, 13 Mar 2022 14:26:25 +0100 Subject: [PATCH 50/54] Support obtaining enum type --- test/types/models.test.ts | 2 ++ test/types/schema.test.ts | 18 ++++++++++++++++-- types/inferschematype.d.ts | 4 +++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 33e03eaf1e9..3d20d9590c2 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -245,6 +245,8 @@ export async function m0_0aModel() { const AutoTypeModelInstance = new AutoTypeModel({}); expectType(AutoTypeModelInstance.userName); + expectType(AutoTypeModelInstance.favoritDrink); + expectType(AutoTypeModelInstance.favoritColorMode); /* -------------------------------------------------------------------------- */ /* Document-Instance-Methods */ diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index f42c5c50d40..a1be8379fc9 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -313,7 +313,9 @@ export type M0_0aAutoTypedSchemaType = { nested?: { age: number; hobby?: string - } + }, + favoritDrink?: 'Tea' | 'Coffee', + favoritColorMode:'dark' | 'light' } , statics: { staticFn:()=>'Returned from staticFn' @@ -339,7 +341,19 @@ export function m0_0aSchema() { 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'; } diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 88ad764fc3d..4ce0a127fc8 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -47,9 +47,11 @@ type ObtainDocumentPathType = PathValueType extends Schema PathValueType extends PathWithTypePropertyBaseType ? Omit : {} >; +type PathEnumOrString = T extends (infer E)[] ? E : T extends { values: any } ? PathEnumOrString : string; + type ResolvePathType = {}> = PathValueType extends (infer Item)[] ? ResolvePathType[] : -PathValueType extends StringConstructor | 'string' | 'String' | typeof Schema.Types.String ? string : +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 : From 52551a90a7357d06dec4dd8e6a5645dafe94916a Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sun, 13 Mar 2022 16:39:49 +0100 Subject: [PATCH 51/54] Support custom typeKey --- test/types/schema.test.ts | 11 +++++++++++ types/index.d.ts | 11 ++++++----- types/inferschematype.d.ts | 26 +++++++++++++------------- types/schemaoptions.d.ts | 8 ++++++-- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index a1be8379fc9..dd1855bb6bc 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -457,5 +457,16 @@ export function m0_0aSchema() { expectType({} as InferredTestSchemaType); + const SchemaWithCustomTypeKey = new Schema({ + name: { + customKTypeKey: String, + required: true + } + }, { + typeKey: 'customKTypeKey' + }); + + expectType({} as InferSchemaType['name']); + return AutoTypedSchema; } diff --git a/types/index.d.ts b/types/index.d.ts index 8059e9b985a..31b5f9615ee 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -758,17 +758,18 @@ export function model( 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 = any, - DocType extends ObtainDocumentType = any, + class Schema, TInstanceMethods = {}, TQueryHelpers = any, + PathTypeKey extends TypeKeyBaseType = DefaultTypeKey, + DocType extends ObtainDocumentType = any, StaticMethods = {}> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocType, 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) @@ -818,7 +819,7 @@ export function model( 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 index 4ce0a127fc8..94eb4f0d9c0 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -1,20 +1,20 @@ -import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions } from 'mongoose'; +import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions, TypeKeyBaseType } from 'mongoose'; declare module 'mongoose' { - type ObtainDocumentType = + type ObtainDocumentType = DoesDocTypeExist extends true ? EnforcedDocType : { [K in keyof (RequiredPaths & - OptionalPaths)]: ObtainDocumentPathType; + OptionalPaths)]: ObtainDocumentPathType; }; - type InferSchemaType = SchemaType extends Schema - ? DoesDocTypeExist extends true ? DocType : DocDefinition + type InferSchemaType = SchemaType extends Schema + ? DoesDocTypeExist extends true ? DocType : ObtainSchemaGeneric : unknown; - type ObtainSchemaGeneric = - TSchema extends Schema - ? { DocType: DocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, DocDefinition: DocDefinition, StaticMethods: StaticMethods }[name] + type ObtainSchemaGeneric = + TSchema extends Schema + ? { EnforcedDocType: EnforcedDocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, PathTypeKey:PathTypeKey, DocType: DocType, StaticMethods: StaticMethods }[name] : never; } @@ -22,7 +22,7 @@ type DoesDocTypeExist = keyof DocType extends string ? true : false; type RequiredPathBaseType = { required: true | [true, string | undefined] } -type PathWithTypePropertyBaseType = { type: any } +type PathWithTypePropertyBaseType = { [k in TypeKey]: any } type RequiredPathKeys = { [K in keyof T]: T[K] extends RequiredPathBaseType ? K : never; @@ -40,11 +40,11 @@ type OptionalPaths = { [K in OptionalPathKeys]?: T[K]; }; -type ObtainDocumentPathType = PathValueType extends Schema +type ObtainDocumentPathType = PathValueType extends Schema ? InferSchemaType : ResolvePathType< - PathValueType extends PathWithTypePropertyBaseType ? PathValueType['type'] : PathValueType, - PathValueType extends PathWithTypePropertyBaseType ? Omit : {} + PathValueType extends PathWithTypePropertyBaseType ? PathValueType[typeKey] : PathValueType, + PathValueType extends PathWithTypePropertyBaseType ? Omit : {} >; type PathEnumOrString = T extends (infer E)[] ? E : T extends { values: any } ? PathEnumOrString : string; @@ -61,4 +61,4 @@ PathValueType extends ObjectConstructor | typeof Schema.Types.Mixed ? Schema.Typ keyof PathValueType extends never ? Schema.Types.Mixed : PathValueType extends MapConstructor ? Map> : PathValueType extends typeof SchemaType ? PathValueType['prototype'] : -PathValueType \ No newline at end of file +unknown \ No newline at end of file diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 91a34e2d5b5..291fc71537e 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 | number | symbol; + + 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 From 605a089f93b02b76a5875f9489b1cd1f6d10b373 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Sun, 13 Mar 2022 19:32:24 +0100 Subject: [PATCH 52/54] Refact previous commit --- types/inferschematype.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 94eb4f0d9c0..fc578d453d9 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -2,7 +2,7 @@ import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions, TypeKeyBaseType declare module 'mongoose' { - type ObtainDocumentType = + type ObtainDocumentType = DoesDocTypeExist extends true ? EnforcedDocType : { [K in keyof (RequiredPaths & OptionalPaths)]: ObtainDocumentPathType; From 797e1dcecd5e5ffa81f5d914295138d9ee300f92 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Tue, 22 Mar 2022 22:39:42 +0100 Subject: [PATCH 53/54] Provide JSDoc comments for inferschematype types. --- types/index.d.ts | 10 ++-- types/inferschematype.d.ts | 102 +++++++++++++++++++++++++++++++------ types/schemaoptions.d.ts | 2 +- 3 files changed, 92 insertions(+), 22 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 31b5f9615ee..598e5aa807c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -253,7 +253,7 @@ export function model( } export const Model: Model; - type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { + type Model = NodeJS.EventEmitter & AcceptsDiscriminator & ObtainSchemaGeneric & { new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument; @@ -759,14 +759,14 @@ export function model( export type ErrorHandlingMiddlewareFunction = (this: ThisType, err: NativeError, res: ResType, next: CallbackWithoutResultAndOptionalError) => void; class Schema, TInstanceMethods = {}, TQueryHelpers = any, - PathTypeKey extends TypeKeyBaseType = DefaultTypeKey, - DocType extends ObtainDocumentType = any, - StaticMethods = {}> + TPathTypeKey extends TypeKeyBaseType = DefaultTypeKey, + DocType extends ObtainDocumentType = any, + TStaticMethods = {}> extends events.EventEmitter { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); + constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions); /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index fc578d453d9..6894145e512 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -1,54 +1,124 @@ import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions, TypeKeyBaseType } from 'mongoose'; declare module 'mongoose' { - - type ObtainDocumentType = - DoesDocTypeExist extends true ? EnforcedDocType : { + /** + * @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; + 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 - ? DoesDocTypeExist extends true ? DocType : ObtainSchemaGeneric + ? IsItRecordAndNotAny extends true ? DocType : ObtainSchemaGeneric : unknown; - type ObtainSchemaGeneric = - TSchema extends Schema - ? { EnforcedDocType: EnforcedDocType, M: M, TInstanceMethods: TInstanceMethods, TQueryHelpers: TQueryHelpers, PathTypeKey:PathTypeKey, DocType: DocType, StaticMethods: StaticMethods }[name] - : never; + /** + * @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; -type DoesDocTypeExist = keyof DocType extends string ? 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] } -type PathWithTypePropertyBaseType = { [k in TypeKey]: any } +/** + * @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]; }; -type ObtainDocumentPathType = PathValueType extends Schema +/** + * @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 : {} + PathValueType extends PathWithTypePropertyBaseType ? PathValueType[TypeKey] : PathValueType, + PathValueType extends PathWithTypePropertyBaseType ? Omit : {} >; -type PathEnumOrString = T extends (infer E)[] ? E : T extends { values: any } ? PathEnumOrString : string; +/** + * @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 : diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 291fc71537e..b9863ed52cc 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -8,7 +8,7 @@ declare module 'mongoose' { currentTime?: () => (NativeDate | number); } - type TypeKeyBaseType = string | number | symbol; + type TypeKeyBaseType = string; type DefaultTypeKey = 'type' From bc4953c7d182a69927f3dd67e12359255a79bbf7 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Wed, 23 Mar 2022 01:03:51 +0100 Subject: [PATCH 54/54] Merge branch 'master' of https://github.com/Automattic/mongoose --- test/document.test.js | 2 +- test/types/document.test.ts | 8 +++++++- types/document.d.ts | 2 +- types/index.d.ts | 4 ++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/test/document.test.js b/test/document.test.js index 3eb8943585c..10dde42e7b8 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -11199,7 +11199,7 @@ describe('document', function() { await doc.validate(); }); - it('should give an instance function back rather than undefined', function M0_0aModelJS() { + 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({}); 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/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 598e5aa807c..8ac0b20c5a1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -758,9 +758,9 @@ export function model( 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 = any, + class Schema, TInstanceMethods = {}, TQueryHelpers = {}, TPathTypeKey extends TypeKeyBaseType = DefaultTypeKey, - DocType extends ObtainDocumentType = any, + DocType extends ObtainDocumentType = ObtainDocumentType, TStaticMethods = {}> extends events.EventEmitter { /**