Skip to content

Commit

Permalink
Merge branch '6.5' of github.com:Automattic/mongoose into 6.5
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed Jul 20, 2022
2 parents 4af3dc4 + 6fcc0dc commit e9ad0b8
Show file tree
Hide file tree
Showing 15 changed files with 237 additions and 31 deletions.
38 changes: 38 additions & 0 deletions docs/guide.md
Expand Up @@ -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;
});
Expand All @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions 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`.

Expand Down
22 changes: 20 additions & 2 deletions lib/schema.js
Expand Up @@ -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);

Expand Down Expand Up @@ -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 ?
Expand All @@ -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));

Expand Down
1 change: 1 addition & 0 deletions lib/validoptions.js
Expand Up @@ -15,6 +15,7 @@ const VALID_OPTIONS = Object.freeze([
'bufferTimeoutMS',
'cloneSchemas',
'debug',
'id',
'timestamps.createdAt.immutable',
'maxTimeMS',
'objectIdGetter',
Expand Down
18 changes: 18 additions & 0 deletions test/index.test.js
Expand Up @@ -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);
});
});
});
40 changes: 40 additions & 0 deletions test/schema.test.js
Expand Up @@ -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);
});
});
2 changes: 1 addition & 1 deletion test/types/schema.test.ts
Expand Up @@ -550,7 +550,7 @@ export type AutoTypedSchemaType = {
},
methods: {
instanceFn: () => 'Returned from DocumentInstanceFn'
},
}
};

// discriminator
Expand Down
36 changes: 35 additions & 1 deletion 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 {
Expand Down Expand Up @@ -86,3 +86,37 @@ function gh11543() {

expectType<PetVirtuals>(personSchema.virtuals);
}

function autoTypedVirtuals() {
type AutoTypedSchemaType = InferSchemaType<typeof testSchema>;
type VirtualsType = { domain: string };
type InferredDocType = FlatRecord<AutoTypedSchemaType & ObtainSchemaGeneric<typeof testSchema, 'TVirtuals'>>;

const testSchema = new Schema({
email: {
type: String,
required: [true, 'email is required']
}
}, {
virtuals: {
domain: {
get() {
expectType<Document<any, any, { email: string }> & AutoTypedSchemaType>(this);
return this.email.slice(this.email.indexOf('@') + 1);
},
set() {
expectType<Document<any, any, AutoTypedSchemaType> & AutoTypedSchemaType>(this);
},
options: {}
}
}
});


const TestModel = model('AutoTypedVirtuals', testSchema);

const doc = new TestModel();
expectType<string>(doc.domain);

expectType<FlatRecord<AutoTypedSchemaType & VirtualsType >>({} as InferredDocType);
}
5 changes: 3 additions & 2 deletions types/index.d.ts
Expand Up @@ -21,6 +21,7 @@
/// <reference path="./utility.d.ts" />
/// <reference path="./validation.d.ts" />
/// <reference path="./inferschematype.d.ts" />
/// <reference path="./virtuals.d.ts" />

declare class NativeDate extends global.Date { }

Expand Down Expand Up @@ -156,15 +157,15 @@ declare module 'mongoose' {

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

export class Schema<EnforcedDocType = any, M = Model<EnforcedDocType, any, any, any>, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = any,
export class Schema<EnforcedDocType = any, M = Model<EnforcedDocType, any, any, any>, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = {},
TStaticMethods = {},
TPathTypeKey extends TypeKeyBaseType = DefaultTypeKey,
DocType extends ObtainDocumentType<DocType, EnforcedDocType, TPathTypeKey> = ObtainDocumentType<any, EnforcedDocType, TPathTypeKey>>
extends events.EventEmitter {
/**
* Create a new schema
*/
constructor(definition?: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>> | DocType, options?: SchemaOptions<TPathTypeKey, FlatRecord<DocType>, TInstanceMethods, TQueryHelpers, TStaticMethods>);
constructor(definition?: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>> | DocType, options?: SchemaOptions<TPathTypeKey, FlatRecord<DocType>, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals>);

/** Adds key path / schema type pairs to this schema. */
add(obj: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>> | Schema, prefix?: string): this;
Expand Down
23 changes: 3 additions & 20 deletions types/inferschematype.d.ts
Expand Up @@ -11,7 +11,8 @@ import {
DateSchemaDefinition,
ObtainDocumentType,
DefaultTypeKey,
ObjectIdSchemaDefinition
ObjectIdSchemaDefinition,
IfEquals
} from 'mongoose';

declare module 'mongoose' {
Expand All @@ -37,7 +38,7 @@ declare module 'mongoose' {
* // result
* type UserType = {userName?: string}
*/
type InferSchemaType<SchemaType> = ObtainSchemaGeneric<SchemaType, 'DocType'> ;
type InferSchemaType<SchemaType> = ObtainSchemaGeneric<SchemaType, 'DocType'>;

/**
* @summary Obtains schema Generic type by using generic alias.
Expand All @@ -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<T> = IfEquals<T, any, false, T extends Record<any, any> ? 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<T, U, Y = true, N = false> =
(<G>() => G extends T ? 1 : 0) extends
(<G>() => G extends U ? 1 : 0) ? Y : N;

/**
* @summary Checks if a document path is required or optional.
Expand Down
3 changes: 2 additions & 1 deletion types/models.d.ts
Expand Up @@ -119,7 +119,8 @@ declare module 'mongoose' {
AcceptsDiscriminator,
IndexManager,
SessionStarter {
new <DocType = T>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument<T, TMethodsAndOverrides, TVirtuals> & ObtainSchemaGeneric<TSchema, 'TStaticMethods'>;
new <DocType = T>(doc?: DocType, fields?: any | null, options?: boolean | AnyObject): HydratedDocument<T, TMethodsAndOverrides,
IfEquals<TVirtuals, {}, ObtainSchemaGeneric<TSchema, 'TVirtuals'>, TVirtuals>> & ObtainSchemaGeneric<TSchema, 'TStaticMethods'>;

aggregate<R = any>(pipeline?: PipelineStage[], options?: mongodb.AggregateOptions, callback?: Callback<R[]>): Aggregate<Array<R>>;
aggregate<R = any>(pipeline: PipelineStage[], callback?: Callback<R[]>): Aggregate<Array<R>>;
Expand Down
6 changes: 6 additions & 0 deletions types/mongooseoptions.d.ts
Expand Up @@ -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`.
Expand Down
13 changes: 9 additions & 4 deletions types/schemaoptions.d.ts
Expand Up @@ -10,7 +10,7 @@ declare module 'mongoose' {
type TypeKeyBaseType = string;

type DefaultTypeKey = 'type';
interface SchemaOptions<PathTypeKey extends TypeKeyBaseType = DefaultTypeKey, DocType = unknown, InstanceMethods = {}, QueryHelpers = {}, StaticMethods = {}, virtuals = {}> {
interface SchemaOptions<PathTypeKey extends TypeKeyBaseType = DefaultTypeKey, DocType = unknown, TInstanceMethods = {}, QueryHelpers = {}, TStaticMethods = {}, TVirtuals = {}> {
/**
* 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
Expand Down Expand Up @@ -191,15 +191,15 @@ declare module 'mongoose' {
/**
* Model Statics methods.
*/
statics?: Record<any, (this: Model<DocType>, ...args: any) => unknown> | StaticMethods,
statics?: Record<any, (this: Model<DocType>, ...args: any) => unknown> | TStaticMethods,

/**
* Document instance methods.
*/
methods?: Record<any, (this: HydratedDocument<DocType>, ...args: any) => unknown> | InstanceMethods,
methods?: Record<any, (this: HydratedDocument<DocType>, ...args: any) => unknown> | TInstanceMethods,

/**
* Query helper functions
* Query helper functions.
*/
query?: Record<any, <T extends QueryWithHelpers<unknown, DocType>>(this: T, ...args: any) => T> | QueryHelpers,

Expand All @@ -208,5 +208,10 @@ declare module 'mongoose' {
* @default true
*/
castNonArrays?: boolean;

/**
* Virtual paths.
*/
virtuals?: SchemaOptionsVirtualsPropertyType<DocType, TVirtuals, TInstanceMethods>,
}
}

0 comments on commit e9ad0b8

Please sign in to comment.