diff --git a/docs/guide.md b/docs/guide.md index 624cc658836..84db9d7c3f7 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -341,6 +341,23 @@ And what if you want to do some extra processing on the name, like define a `fullName` property that won't get persisted to MongoDB. ```javascript +// That can be done either by adding it to schema options: +const personSchema = new Schema({ + name: { + first: String, + last: String + } +},{ + virtuals:{ + fullName:{ + get() { + return this.name.first + ' ' + this.name.last; + } + } + } +}); + +// Or by using the virtual method as following: personSchema.virtual('fullName').get(function() { return this.name.first + ' ' + this.name.last; }); @@ -363,6 +380,27 @@ You can also add a custom setter to your virtual that will let you set both first name and last name via the `fullName` virtual. ```javascript +// Again that can be done either by adding it to schema options: +const personSchema = new Schema({ + name: { + first: String, + last: String + } +},{ + virtuals:{ + fullName:{ + get() { + return this.name.first + ' ' + this.name.last; + } + set(v) { + this.name.first = v.substr(0, v.indexOf(' ')); + this.name.last = v.substr(v.indexOf(' ') + 1); + } + } + } +}); + +// Or by using the virtual method as following: personSchema.virtual('fullName'). get(function() { return this.name.first + ' ' + this.name.last; diff --git a/docs/typescript/virtuals.md b/docs/typescript/virtuals.md index b9ccc9cacdf..b73602960a2 100644 --- a/docs/typescript/virtuals.md +++ b/docs/typescript/virtuals.md @@ -1,6 +1,34 @@ # Virtuals in TypeScript [Virtuals](/docs/tutorials/virtuals.html) are computed properties: you can access virtuals on hydrated Mongoose documents, but virtuals are **not** stored in MongoDB. +Mongoose supports auto typed virtuals so you don't need to define additional typescript interface anymore but you are still able to do so. + +### Automatically Inferred Types: + +To make mongoose able to infer virtuals type, You have to define them in schema constructor as following: + +```ts +import { Schema, Model, model } from 'mongoose'; + +const schema = new Schema( + { + firstName: String, + lastName: String, + }, + { + virtuals:{ + fullName:{ + get(){ + return `${this.firstName} ${this.lastName}`; + } + // virtual setter and options can be defined here as well. + } + } + } +); +``` + +### Set virtuals type manually: You shouldn't define virtuals in your TypeScript [document interface](/docs/typescript.html). Instead, you should define a separate interface for your virtuals, and pass this interface to `Model` and `Schema`. diff --git a/lib/schema.js b/lib/schema.js index 730bddeac49..cb194bb9599 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -133,6 +133,24 @@ function Schema(obj, options) { this.add(obj); } + // build virtual paths + if (options && options.virtuals) { + const virtuals = options.virtuals; + const pathNames = Object.keys(virtuals); + for (const pathName of pathNames) { + const pathOptions = virtuals[pathName].options ? virtuals[pathName].options : undefined; + const virtual = this.virtual(pathName, pathOptions); + + if (virtuals[pathName].get) { + virtual.get(virtuals[pathName].get); + } + + if (virtuals[pathName].set) { + virtual.set(virtuals[pathName].set); + } + } + } + // check if _id's value is a subdocument (gh-2276) const _idSubDoc = obj && obj._id && utils.isObject(obj._id); @@ -438,8 +456,8 @@ Schema.prototype.pick = function(paths, options) { Schema.prototype.defaultOptions = function(options) { this._userProvidedOptions = options == null ? {} : utils.clone(options); const baseOptions = this.base && this.base.options || {}; - const strict = 'strict' in baseOptions ? baseOptions.strict : true; + const id = 'id' in baseOptions ? baseOptions.id : true; options = utils.options({ strict: strict, strictQuery: 'strict' in this._userProvidedOptions ? @@ -458,7 +476,7 @@ Schema.prototype.defaultOptions = function(options) { validateBeforeSave: true, // the following are only applied at construction time _id: true, - id: true, + id: id, typeKey: 'type' }, utils.clone(options)); diff --git a/lib/validoptions.js b/lib/validoptions.js index 0f7b697c415..a42e552c7af 100644 --- a/lib/validoptions.js +++ b/lib/validoptions.js @@ -15,6 +15,7 @@ const VALID_OPTIONS = Object.freeze([ 'bufferTimeoutMS', 'cloneSchemas', 'debug', + 'id', 'timestamps.createdAt.immutable', 'maxTimeMS', 'objectIdGetter', diff --git a/test/index.test.js b/test/index.test.js index 9c80b5ae644..831b817f7a5 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1086,4 +1086,22 @@ describe('mongoose module:', function() { assert.deepEqual(res.toObject(), { answer: 42 }); }); }); + describe('global id option', function() { + it('can disable the id virtual on schemas gh-11966', async function() { + const m = new mongoose.Mongoose(); + m.set('id', false); + + const db = await m.connect(start.uri); + + const schema = new m.Schema({ title: String }); + + const falseID = db.model('gh11966', schema); + + + const entry = await falseID.create({ + title: 'The IDless master' + }); + assert.equal(entry.id, undefined); + }); + }); }); diff --git a/test/schema.test.js b/test/schema.test.js index 56c55fe8964..58d03cfb905 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -2815,4 +2815,44 @@ describe('schema', function() { assert.ok({}.toString()); }); + + it('enable defining virtual paths by using schema constructor (gh-11908)', async function() { + function get() {return this.email.slice(this.email.indexOf('@') + 1);} + function set(v) { this.email = [this.email.slice(0, this.email.indexOf('@')), v].join('@');} + const options = { + getters: true + }; + + const definition = { + email: { type: String } + }; + const TestSchema1 = new Schema(definition); + TestSchema1.virtual('domain', options).set(set).get(get); + + const TestSchema2 = new Schema({ + email: { type: String } + }, { + virtuals: { + domain: { + get, + set, + options + } + } + }); + + assert.deepEqual(TestSchema2.virtuals, TestSchema1.virtuals); + + const doc1 = new (mongoose.model('schema1', TestSchema1))({ email: 'test@m0_0a.com' }); + const doc2 = new (mongoose.model('schema2', TestSchema2))({ email: 'test@m0_0a.com' }); + + assert.equal(doc1.domain, doc2.domain); + + const mongooseDomain = 'mongoose.com'; + doc1.domain = mongooseDomain; + doc2.domain = mongooseDomain; + + assert.equal(doc1.domain, mongooseDomain); + assert.equal(doc1.domain, doc2.domain); + }); }); diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 6843e12cfaa..8a0dd423801 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -550,7 +550,7 @@ export type AutoTypedSchemaType = { }, methods: { instanceFn: () => 'Returned from DocumentInstanceFn' - }, + } }; // discriminator diff --git a/test/types/virtuals.test.ts b/test/types/virtuals.test.ts index 2011634bd5f..a5b07f54419 100644 --- a/test/types/virtuals.test.ts +++ b/test/types/virtuals.test.ts @@ -1,4 +1,4 @@ -import { Document, Model, Schema, model } from 'mongoose'; +import { Document, Model, Schema, model, InferSchemaType, FlatRecord, ObtainSchemaGeneric } from 'mongoose'; import { expectType } from 'tsd'; interface IPerson { @@ -86,3 +86,37 @@ function gh11543() { expectType(personSchema.virtuals); } + +function autoTypedVirtuals() { + type AutoTypedSchemaType = InferSchemaType; + type VirtualsType = { domain: string }; + type InferredDocType = FlatRecord>; + + const testSchema = new Schema({ + email: { + type: String, + required: [true, 'email is required'] + } + }, { + virtuals: { + domain: { + get() { + expectType & AutoTypedSchemaType>(this); + return this.email.slice(this.email.indexOf('@') + 1); + }, + set() { + expectType & AutoTypedSchemaType>(this); + }, + options: {} + } + } + }); + + + const TestModel = model('AutoTypedVirtuals', testSchema); + + const doc = new TestModel(); + expectType(doc.domain); + + expectType>({} as InferredDocType); +} diff --git a/types/index.d.ts b/types/index.d.ts index 3cd944d0d8f..24c358f9a95 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -21,6 +21,7 @@ /// /// /// +/// declare class NativeDate extends global.Date { } @@ -156,7 +157,7 @@ declare module 'mongoose' { type QueryResultType = T extends Query ? ResultType : never; - export class Schema, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = any, + export class Schema, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = {}, TStaticMethods = {}, TPathTypeKey extends TypeKeyBaseType = DefaultTypeKey, DocType extends ObtainDocumentType = ObtainDocumentType> @@ -164,7 +165,7 @@ declare module 'mongoose' { /** * Create a new schema */ - constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions, TInstanceMethods, TQueryHelpers, TStaticMethods>); + constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals>); /** 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 b32a97b9ef7..5041f830477 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -11,7 +11,8 @@ import { DateSchemaDefinition, ObtainDocumentType, DefaultTypeKey, - ObjectIdSchemaDefinition + ObjectIdSchemaDefinition, + IfEquals } from 'mongoose'; declare module 'mongoose' { @@ -37,7 +38,7 @@ declare module 'mongoose' { * // result * type UserType = {userName?: string} */ - type InferSchemaType = ObtainSchemaGeneric ; + type InferSchemaType = ObtainSchemaGeneric; /** * @summary Obtains schema Generic type by using generic alias. @@ -58,24 +59,6 @@ declare module 'mongoose' { }[alias] : unknown; } -/** - * @summary Checks if a type is "Record" or "any". - * @description It Helps to check if user has provided schema type "EnforcedDocType" - * @param {T} T A generic type to be checked. - * @returns true if {@link T} is Record OR false if {@link T} is of any type. - */ -type IsItRecordAndNotAny = IfEquals ? true : false>; - -/** - * @summary Checks if two types are identical. - * @param {T} T The first type to be compared with {@link U}. - * @param {U} U The seconde type to be compared with {@link T}. - * @param {Y} Y A type to be returned if {@link T} & {@link U} are identical. - * @param {N} N A type to be returned if {@link T} & {@link U} are not identical. - */ -type IfEquals = - (() => G extends T ? 1 : 0) extends - (() => G extends U ? 1 : 0) ? Y : N; /** * @summary Checks if a document path is required or optional. diff --git a/types/models.d.ts b/types/models.d.ts index 9fae6afa66e..17385597a17 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -119,7 +119,8 @@ declare module 'mongoose' { AcceptsDiscriminator, IndexManager, SessionStarter { - new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument & ObtainSchemaGeneric; + new (doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument, TVirtuals>> & ObtainSchemaGeneric; aggregate(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback): Aggregate>; aggregate(pipeline: PipelineStage[], callback?: Callback): Aggregate>; diff --git a/types/mongooseoptions.d.ts b/types/mongooseoptions.d.ts index dabae7e5333..b470e1aa9b6 100644 --- a/types/mongooseoptions.d.ts +++ b/types/mongooseoptions.d.ts @@ -79,6 +79,12 @@ declare module 'mongoose' { | stream.Writable | ((collectionName: string, methodName: string, ...methodArgs: any[]) => void); + /** + * If `true`, adds a `id` virtual to all schemas unless overwritten on a per-schema basis. + * @defaultValue true + */ + id?: boolean; + /** * If `false`, it will change the `createdAt` field to be [`immutable: false`](https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-immutable) * which means you can update the `createdAt`. diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 02791ab4171..1a87a8ccf9f 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -10,7 +10,7 @@ declare module 'mongoose' { type TypeKeyBaseType = string; type DefaultTypeKey = 'type'; - 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 @@ -191,15 +191,15 @@ declare module 'mongoose' { /** * Model Statics methods. */ - statics?: Record, ...args: any) => unknown> | StaticMethods, + statics?: Record, ...args: any) => unknown> | TStaticMethods, /** * Document instance methods. */ - methods?: Record, ...args: any) => unknown> | InstanceMethods, + methods?: Record, ...args: any) => unknown> | TInstanceMethods, /** - * Query helper functions + * Query helper functions. */ query?: Record>(this: T, ...args: any) => T> | QueryHelpers, @@ -208,5 +208,10 @@ declare module 'mongoose' { * @default true */ castNonArrays?: boolean; + + /** + * Virtual paths. + */ + virtuals?: SchemaOptionsVirtualsPropertyType, } } diff --git a/types/utility.d.ts b/types/utility.d.ts index 79bb6ad8361..bfc531aa8f2 100644 --- a/types/utility.d.ts +++ b/types/utility.d.ts @@ -19,4 +19,23 @@ declare module 'mongoose' { */ type FlatRecord = { [K in keyof T]: T[K] }; + /** + * @summary Checks if a type is "Record" or "any". + * @description It Helps to check if user has provided schema type "EnforcedDocType" + * @param {T} T A generic type to be checked. + * @returns true if {@link T} is Record OR false if {@link T} is of any type. + */ +type IsItRecordAndNotAny = IfEquals ? true : false>; + +/** + * @summary Checks if two types are identical. + * @param {T} T The first type to be compared with {@link U}. + * @param {U} U The seconde type to be compared with {@link T}. + * @param {Y} Y A type to be returned if {@link T} & {@link U} are identical. + * @param {N} N A type to be returned if {@link T} & {@link U} are not identical. + */ +type IfEquals = + (() => G extends T ? 1 : 0) extends + (() => G extends U ? 1 : 0) ? Y : N; + } diff --git a/types/virtuals.d.ts b/types/virtuals.d.ts new file mode 100644 index 00000000000..2ec48a496f1 --- /dev/null +++ b/types/virtuals.d.ts @@ -0,0 +1,14 @@ +declare module 'mongoose' { + type VirtualPathFunctions = { + get?: TVirtualPathFN; + set?: TVirtualPathFN; + options?: VirtualTypeOptions, DocType>; + }; + + type TVirtualPathFN = + >(this: Document & DocType, value: PathType, virtual: VirtualType, doc: Document & DocType) => TReturn; + + type SchemaOptionsVirtualsPropertyType, TInstanceMethods = {}> = { + [K in keyof VirtualPaths]: VirtualPathFunctions extends true ? DocType : any, VirtualPaths[K], TInstanceMethods> + }; +}