Skip to content

Commit 0c92558

Browse files
authored
Merge pull request #11908 from mohammad0-0ahmad-forks/auto-typed-virtuals
[✔️] Auto-typed-virtuals
2 parents 067e6fe + 917d331 commit 0c92558

File tree

12 files changed

+210
-29
lines changed

12 files changed

+210
-29
lines changed

docs/guide.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,23 @@ And what if you want to do some extra processing on the name, like
341341
define a `fullName` property that won't get persisted to MongoDB.
342342

343343
```javascript
344+
// That can be done either by adding it to schema options:
345+
const personSchema = new Schema({
346+
name: {
347+
first: String,
348+
last: String
349+
}
350+
},{
351+
virtuals:{
352+
fullName:{
353+
get() {
354+
return this.name.first + ' ' + this.name.last;
355+
}
356+
}
357+
}
358+
});
359+
360+
// Or by using the virtual method as following:
344361
personSchema.virtual('fullName').get(function() {
345362
return this.name.first + ' ' + this.name.last;
346363
});
@@ -363,6 +380,27 @@ You can also add a custom setter to your virtual that will let you set both
363380
first name and last name via the `fullName` virtual.
364381

365382
```javascript
383+
// Again that can be done either by adding it to schema options:
384+
const personSchema = new Schema({
385+
name: {
386+
first: String,
387+
last: String
388+
}
389+
},{
390+
virtuals:{
391+
fullName:{
392+
get() {
393+
return this.name.first + ' ' + this.name.last;
394+
}
395+
set(v) {
396+
this.name.first = v.substr(0, v.indexOf(' '));
397+
this.name.last = v.substr(v.indexOf(' ') + 1);
398+
}
399+
}
400+
}
401+
});
402+
403+
// Or by using the virtual method as following:
366404
personSchema.virtual('fullName').
367405
get(function() {
368406
return this.name.first + ' ' + this.name.last;

docs/typescript/virtuals.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,34 @@
11
# Virtuals in TypeScript
22

33
[Virtuals](/docs/tutorials/virtuals.html) are computed properties: you can access virtuals on hydrated Mongoose documents, but virtuals are **not** stored in MongoDB.
4+
Mongoose supports auto typed virtuals so you don't need to define additional typescript interface anymore but you are still able to do so.
5+
6+
### Automatically Inferred Types:
7+
8+
To make mongoose able to infer virtuals type, You have to define them in schema constructor as following:
9+
10+
```ts
11+
import { Schema, Model, model } from 'mongoose';
12+
13+
const schema = new Schema(
14+
{
15+
firstName: String,
16+
lastName: String,
17+
},
18+
{
19+
virtuals:{
20+
fullName:{
21+
get(){
22+
return `${this.firstName} ${this.lastName}`;
23+
}
24+
// virtual setter and options can be defined here as well.
25+
}
26+
}
27+
}
28+
);
29+
```
30+
31+
### Set virtuals type manually:
432
You shouldn't define virtuals in your TypeScript [document interface](/docs/typescript.html).
533
Instead, you should define a separate interface for your virtuals, and pass this interface to `Model` and `Schema`.
634

lib/schema.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,24 @@ function Schema(obj, options) {
133133
this.add(obj);
134134
}
135135

136+
// build virtual paths
137+
if (options && options.virtuals) {
138+
const virtuals = options.virtuals;
139+
const pathNames = Object.keys(virtuals);
140+
for (const pathName of pathNames) {
141+
const pathOptions = virtuals[pathName].options ? virtuals[pathName].options : undefined;
142+
const virtual = this.virtual(pathName, pathOptions);
143+
144+
if (virtuals[pathName].get) {
145+
virtual.get(virtuals[pathName].get);
146+
}
147+
148+
if (virtuals[pathName].set) {
149+
virtual.set(virtuals[pathName].set);
150+
}
151+
}
152+
}
153+
136154
// check if _id's value is a subdocument (gh-2276)
137155
const _idSubDoc = obj && obj._id && utils.isObject(obj._id);
138156

test/schema.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2792,4 +2792,44 @@ describe('schema', function() {
27922792
});
27932793
}, /Cannot use schema-level projections.*subdocument_mapping.not_selected/);
27942794
});
2795+
2796+
it('enable defining virtual paths by using schema constructor (gh-11908)', async function() {
2797+
function get() {return this.email.slice(this.email.indexOf('@') + 1);}
2798+
function set(v) { this.email = [this.email.slice(0, this.email.indexOf('@')), v].join('@');}
2799+
const options = {
2800+
getters: true
2801+
};
2802+
2803+
const definition = {
2804+
email: { type: String }
2805+
};
2806+
const TestSchema1 = new Schema(definition);
2807+
TestSchema1.virtual('domain', options).set(set).get(get);
2808+
2809+
const TestSchema2 = new Schema({
2810+
email: { type: String }
2811+
}, {
2812+
virtuals: {
2813+
domain: {
2814+
get,
2815+
set,
2816+
options
2817+
}
2818+
}
2819+
});
2820+
2821+
assert.deepEqual(TestSchema2.virtuals, TestSchema1.virtuals);
2822+
2823+
const doc1 = new (mongoose.model('schema1', TestSchema1))({ email: 'test@m0_0a.com' });
2824+
const doc2 = new (mongoose.model('schema2', TestSchema2))({ email: 'test@m0_0a.com' });
2825+
2826+
assert.equal(doc1.domain, doc2.domain);
2827+
2828+
const mongooseDomain = 'mongoose.com';
2829+
doc1.domain = mongooseDomain;
2830+
doc2.domain = mongooseDomain;
2831+
2832+
assert.equal(doc1.domain, mongooseDomain);
2833+
assert.equal(doc1.domain, doc2.domain);
2834+
});
27952835
});

test/types/schema.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ export type AutoTypedSchemaType = {
550550
},
551551
methods: {
552552
instanceFn: () => 'Returned from DocumentInstanceFn'
553-
},
553+
}
554554
};
555555

556556
// discriminator

test/types/virtuals.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Document, Model, Schema, model } from 'mongoose';
1+
import { Document, Model, Schema, model, InferSchemaType, FlatRecord, ObtainSchemaGeneric } from 'mongoose';
22
import { expectType } from 'tsd';
33

44
interface IPerson {
@@ -86,3 +86,37 @@ function gh11543() {
8686

8787
expectType<PetVirtuals>(personSchema.virtuals);
8888
}
89+
90+
function autoTypedVirtuals() {
91+
type AutoTypedSchemaType = InferSchemaType<typeof testSchema>;
92+
type VirtualsType = { domain: string };
93+
type InferredDocType = FlatRecord<AutoTypedSchemaType & ObtainSchemaGeneric<typeof testSchema, 'TVirtuals'>>;
94+
95+
const testSchema = new Schema({
96+
email: {
97+
type: String,
98+
required: [true, 'email is required']
99+
}
100+
}, {
101+
virtuals: {
102+
domain: {
103+
get() {
104+
expectType<Document<any, any, { email: string }> & AutoTypedSchemaType>(this);
105+
return this.email.slice(this.email.indexOf('@') + 1);
106+
},
107+
set() {
108+
expectType<Document<any, any, AutoTypedSchemaType> & AutoTypedSchemaType>(this);
109+
},
110+
options: {}
111+
}
112+
}
113+
});
114+
115+
116+
const TestModel = model('AutoTypedVirtuals', testSchema);
117+
118+
const doc = new TestModel();
119+
expectType<string>(doc.domain);
120+
121+
expectType<FlatRecord<AutoTypedSchemaType & VirtualsType >>({} as InferredDocType);
122+
}

types/index.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
/// <reference path="./utility.d.ts" />
2222
/// <reference path="./validation.d.ts" />
2323
/// <reference path="./inferschematype.d.ts" />
24+
/// <reference path="./virtuals.d.ts" />
2425

2526
declare class NativeDate extends global.Date { }
2627

@@ -156,15 +157,15 @@ declare module 'mongoose' {
156157

157158
type QueryResultType<T> = T extends Query<infer ResultType, any> ? ResultType : never;
158159

159-
export class Schema<EnforcedDocType = any, M = Model<EnforcedDocType, any, any, any>, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = any,
160+
export class Schema<EnforcedDocType = any, M = Model<EnforcedDocType, any, any, any>, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = {},
160161
TStaticMethods = {},
161162
TPathTypeKey extends TypeKeyBaseType = DefaultTypeKey,
162163
DocType extends ObtainDocumentType<DocType, EnforcedDocType, TPathTypeKey> = ObtainDocumentType<any, EnforcedDocType, TPathTypeKey>>
163164
extends events.EventEmitter {
164165
/**
165166
* Create a new schema
166167
*/
167-
constructor(definition?: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>> | DocType, options?: SchemaOptions<TPathTypeKey, FlatRecord<DocType>, TInstanceMethods, TQueryHelpers, TStaticMethods>);
168+
constructor(definition?: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>> | DocType, options?: SchemaOptions<TPathTypeKey, FlatRecord<DocType>, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals>);
168169

169170
/** Adds key path / schema type pairs to this schema. */
170171
add(obj: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>> | Schema, prefix?: string): this;

types/inferschematype.d.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
DateSchemaDefinition,
1212
ObtainDocumentType,
1313
DefaultTypeKey,
14-
ObjectIdSchemaDefinition
14+
ObjectIdSchemaDefinition,
15+
IfEquals
1516
} from 'mongoose';
1617

1718
declare module 'mongoose' {
@@ -37,7 +38,7 @@ declare module 'mongoose' {
3738
* // result
3839
* type UserType = {userName?: string}
3940
*/
40-
type InferSchemaType<SchemaType> = ObtainSchemaGeneric<SchemaType, 'DocType'> ;
41+
type InferSchemaType<SchemaType> = ObtainSchemaGeneric<SchemaType, 'DocType'>;
4142

4243
/**
4344
* @summary Obtains schema Generic type by using generic alias.
@@ -58,24 +59,6 @@ declare module 'mongoose' {
5859
}[alias]
5960
: unknown;
6061
}
61-
/**
62-
* @summary Checks if a type is "Record" or "any".
63-
* @description It Helps to check if user has provided schema type "EnforcedDocType"
64-
* @param {T} T A generic type to be checked.
65-
* @returns true if {@link T} is Record OR false if {@link T} is of any type.
66-
*/
67-
type IsItRecordAndNotAny<T> = IfEquals<T, any, false, T extends Record<any, any> ? true : false>;
68-
69-
/**
70-
* @summary Checks if two types are identical.
71-
* @param {T} T The first type to be compared with {@link U}.
72-
* @param {U} U The seconde type to be compared with {@link T}.
73-
* @param {Y} Y A type to be returned if {@link T} & {@link U} are identical.
74-
* @param {N} N A type to be returned if {@link T} & {@link U} are not identical.
75-
*/
76-
type IfEquals<T, U, Y = true, N = false> =
77-
(<G>() => G extends T ? 1 : 0) extends
78-
(<G>() => G extends U ? 1 : 0) ? Y : N;
7962

8063
/**
8164
* @summary Checks if a document path is required or optional.

types/models.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ declare module 'mongoose' {
119119
AcceptsDiscriminator,
120120
IndexManager,
121121
SessionStarter {
122-
new <DocType = T>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument<T, TMethodsAndOverrides, TVirtuals> & ObtainSchemaGeneric<TSchema, 'TStaticMethods'>;
122+
new <DocType = T>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument<T, TMethodsAndOverrides,
123+
IfEquals<TVirtuals, {}, ObtainSchemaGeneric<TSchema, 'TVirtuals'>, TVirtuals>> & ObtainSchemaGeneric<TSchema, 'TStaticMethods'>;
123124

124125
aggregate<R = any>(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback<R[]>): Aggregate<Array<R>>;
125126
aggregate<R = any>(pipeline: PipelineStage[], callback?: Callback<R[]>): Aggregate<Array<R>>;

types/schemaoptions.d.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ declare module 'mongoose' {
1010
type TypeKeyBaseType = string;
1111

1212
type DefaultTypeKey = 'type';
13-
interface SchemaOptions<PathTypeKey extends TypeKeyBaseType = DefaultTypeKey, DocType = unknown, InstanceMethods = {}, QueryHelpers = {}, StaticMethods = {}, virtuals = {}> {
13+
interface SchemaOptions<PathTypeKey extends TypeKeyBaseType = DefaultTypeKey, DocType = unknown, TInstanceMethods = {}, QueryHelpers = {}, TStaticMethods = {}, TVirtuals = {}> {
1414
/**
1515
* By default, Mongoose's init() function creates all the indexes defined in your model's schema by
1616
* calling Model.createIndexes() after you successfully connect to MongoDB. If you want to disable
@@ -191,15 +191,15 @@ declare module 'mongoose' {
191191
/**
192192
* Model Statics methods.
193193
*/
194-
statics?: Record<any, (this: Model<DocType>, ...args: any) => unknown> | StaticMethods,
194+
statics?: Record<any, (this: Model<DocType>, ...args: any) => unknown> | TStaticMethods,
195195

196196
/**
197197
* Document instance methods.
198198
*/
199-
methods?: Record<any, (this: HydratedDocument<DocType>, ...args: any) => unknown> | InstanceMethods,
199+
methods?: Record<any, (this: HydratedDocument<DocType>, ...args: any) => unknown> | TInstanceMethods,
200200

201201
/**
202-
* Query helper functions
202+
* Query helper functions.
203203
*/
204204
query?: Record<any, <T extends QueryWithHelpers<unknown, DocType>>(this: T, ...args: any) => T> | QueryHelpers,
205205

@@ -208,5 +208,10 @@ declare module 'mongoose' {
208208
* @default true
209209
*/
210210
castNonArrays?: boolean;
211+
212+
/**
213+
* Virtual paths.
214+
*/
215+
virtuals?: SchemaOptionsVirtualsPropertyType<DocType, TVirtuals, TInstanceMethods>,
211216
}
212217
}

0 commit comments

Comments
 (0)