Skip to content

Commit

Permalink
Merge pull request #11563 from mohammad0-0ahmad-forks/auto-typed-schema
Browse files Browse the repository at this point in the history
Feature: Infer schema type automatically.
  • Loading branch information
vkarpov15 committed Jun 5, 2022
2 parents 32d5933 + 6547fe2 commit f48eee2
Show file tree
Hide file tree
Showing 24 changed files with 985 additions and 123 deletions.
48 changes: 44 additions & 4 deletions docs/guide.md
Expand Up @@ -143,9 +143,18 @@ We may also define our own custom document instance methods.

```javascript
// define a schema
const animalSchema = new Schema({ name: String, type: String });
const animalSchema = new Schema({ name: String, type: String },
{
// Assign a function to the "methods" object of our animalSchema through schema options.
// By following this approach, there is no need to create a separate TS type to define the type of the instance functions.
methods:{
findSimilarTypes(cb){
return mongoose.model('Animal').find({ type: this.type }, cb);
}
}
});

// assign a function to the "methods" object of our animalSchema
// Or, assign a function to the "methods" object of our animalSchema
animalSchema.methods.findSimilarTypes = function(cb) {
return mongoose.model('Animal').find({ type: this.type }, cb);
};
Expand All @@ -169,14 +178,28 @@ dog.findSimilarTypes((err, dogs) => {

<h3 id="statics"><a href="#statics">Statics</a></h3>

You can also add static functions to your model. There are two equivalent
You can also add static functions to your model. There are three equivalent
ways to add a static:

- Add a function property to the second argument of the schema-constructor (`statics`)
- Add a function property to `schema.statics`
- Call the [`Schema#static()` function](/docs/api.html#schema_Schema-static)

```javascript
// Assign a function to the "statics" object of our animalSchema

// define a schema
const animalSchema = new Schema({ name: String, type: String },
{
// Assign a function to the "statics" object of our animalSchema through schema options.
// By following this approach, there is no need to create a separate TS type to define the type of the statics functions.
statics:{
findByName(name){
return this.find({ name: new RegExp(name, 'i') });
}
}
});

// Or, Assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function(name) {
return this.find({ name: new RegExp(name, 'i') });
};
Expand All @@ -197,6 +220,20 @@ but for mongoose queries. Query helper methods let you extend mongoose's
[chainable query builder API](./queries.html).

```javascript

// define a schema
const animalSchema = new Schema({ name: String, type: String },
{
// Assign a function to the "query" object of our animalSchema through schema options.
// By following this approach, there is no need to create a separate TS type to define the type of the query functions.
query:{
byName(name){
return this.where({ name: new RegExp(name, 'i') })
}
}
});

// Or, Assign a function to the "query" object of our animalSchema
animalSchema.query.byName = function(name) {
return this.where({ name: new RegExp(name, 'i') })
};
Expand Down Expand Up @@ -409,6 +446,7 @@ Valid options:
- [read](#read)
- [writeConcern](#writeConcern)
- [shardKey](#shardKey)
- [statics](#statics)
- [strict](#strict)
- [strictQuery](#strictQuery)
- [toJSON](#toJSON)
Expand All @@ -423,6 +461,8 @@ Valid options:
- [skipVersioning](#skipVersioning)
- [timestamps](#timestamps)
- [storeSubdocValidationError](#storeSubdocValidationError)
- [methods](#methods)
- [query](#query-helpers)

<h3 id="autoIndex"><a href="#autoIndex">option: autoIndex</a></h3>

Expand Down
33 changes: 32 additions & 1 deletion docs/typescript/query-helpers.md
Expand Up @@ -14,7 +14,10 @@ var Project = mongoose.model('Project', ProjectSchema);
Project.find().where('stars').gt(1000).byName('mongoose');
```

In TypeScript, Mongoose's `Model` takes 3 generic parameters:
In TypeScript, Mongoose does support manually typed and automatically typed Query Helpers.

1- Manually typed:
Mongoose's `Model` takes 3 generic parameters:

1. The `DocType`
2. a `TQueryHelpers` type
Expand Down Expand Up @@ -58,4 +61,32 @@ async function run(): Promise<void> {
// Equivalent to `ProjectModel.find({ stars: { $gt: 1000 }, name: 'mongoose' })`
await ProjectModel.find().where('stars').gt(1000).byName('mongoose');
}
```

2- Automatically typed:
Mongoose does support auto typed Query Helpers that it are supplied in schema options.
Query Helpers functions can be defined as following:

```typescript
import { Schema, model } from 'mongoose';

const schema = new Schema({
name: { type: String, required: true },
stars: { type: Number, required: true }
}, {
query: {
byName(name) {
return this.find({ name: name });
}
}
});

const ProjectModel = model(
'Project',
schema
);

// Equivalent to `ProjectModel.find({ stars: { $gt: 1000 }, name: 'mongoose' })`
await ProjectModel.find().where('stars').gt(1000).byName('mongoose');
}
```
49 changes: 42 additions & 7 deletions docs/typescript/schemas.md
@@ -1,7 +1,11 @@
# Schemas in TypeScript

Mongoose [schemas](/docs/guide.html) are how you tell Mongoose what your documents look like.
Mongoose schemas are separate from TypeScript interfaces, so you need to define both a _document interface_ and a _schema_.
Mongoose schemas are separate from TypeScript interfaces, so you need to define both a _document interface_ and a _schema_ until V6.3.1.
Mongoose supports auto typed schemas so you don't need to define additional typescript interface anymore but you are still able to do so.
Mongoose provides a `InferSchemaType`, which infers the type of the auto typed schema document when needed.

`Until mongoose V6.3.1:`

```typescript
import { Schema } from 'mongoose';
Expand All @@ -21,6 +25,37 @@ const schema = new Schema<User>({
});
```

`another approach:`

```typescript
import { Schema, InferSchemaType } from 'mongoose';

// Document interface
// No need to define TS interface any more.
// interface User {
// name: string;
// email: string;
// avatar?: string;
// }

// Schema
const schema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true },
avatar: String
});

type User = InferSchemaType<typeof schema>;
// InferSchemaType will determine the type as follows:
// type User = {
// name: string;
// email: string;
// avatar?: string;
// }


```

By default, Mongoose does **not** check if your document interface lines up with your schema.
For example, the above code won't throw an error if `email` is optional in the document interface, but `required` in `schema`.

Expand Down Expand Up @@ -51,7 +86,7 @@ Mongoose wraps `DocType` in a Mongoose document for cases like the `this` parame
For example:

```typescript
schema.pre('save', function(): void {
schema.pre('save', function (): void {
console.log(this.name); // TypeScript knows that `this` is a `mongoose.Document & User` by default
});
```
Expand Down Expand Up @@ -102,7 +137,7 @@ interface User {
const schema = new Schema<User, Model<User>>({
name: { type: String, required: true },
email: { type: String, required: true },
avatar: String
avatar: String,
});
```

Expand All @@ -121,13 +156,13 @@ interface BlogPost {
}

interface User {
tags: Types.Array<string>,
blogPosts: Types.DocumentArray<BlogPost>
tags: Types.Array<string>;
blogPosts: Types.DocumentArray<BlogPost>;
}

const schema = new Schema<User, Model<User>>({
tags: [String],
blogPosts: [{ title: String }]
blogPosts: [{ title: String }],
});
```

Expand All @@ -139,4 +174,4 @@ If you use `Types.DocumentArray` in the above case, you'll be able to `push()` a
const user = new User({ blogPosts: [] });

user.blogPosts.push({ title: 'test' }); // Would not work if you did `blogPosts: BlogPost[]`
```
```
21 changes: 21 additions & 0 deletions docs/typescript/statics-and-methods.md
Expand Up @@ -65,6 +65,27 @@ const User = model<IUser, UserModel>('User', schema);
const answer: number = User.myStaticMethod(); // 42
```

Mongoose does support auto typed static functions now that it is supplied in schema options.
Statics functions can be defined as following:

```typescript
import { Schema, model } from 'mongoose';

const schema = new Schema(
{ name: String },
{
statics: {
myStaticMethod() {
return 42;
},
},
}
);

const User = model('User', schema);

const answer = User.myStaticMethod(); // 42
```
## Both Methods and Statics

Below is how you can define a model that has both methods and statics.
Expand Down
47 changes: 47 additions & 0 deletions docs/typescript/statics.md
@@ -0,0 +1,47 @@
# Statics in TypeScript

Mongoose [models](/docs/models.html) do **not** have an explicit generic parameter for [statics](/docs/guide.html#statics).
If your model has statics, we recommend creating an interface that [extends](https://www.typescriptlang.org/docs/handbook/interfaces.html) Mongoose's `Model` interface as shown below.

```typescript
import { Model, Schema, model } from 'mongoose';

interface IUser {
name: string;
}

interface UserModel extends Model<IUser> {
myStaticMethod(): number;
}

const schema = new Schema<IUser, UserModel>({ name: String });
schema.static('myStaticMethod', function myStaticMethod() {
return 42;
});

const User = model<IUser, UserModel>('User', schema);

const answer: number = User.myStaticMethod(); // 42
```

Mongoose does support auto typed static functions that it are supplied in schema options.
Static functions can be defined by:

```typescript
import { Schema, model } from 'mongoose';

const schema = new Schema(
{ name: String },
{
statics: {
myStaticMethod() {
return 42;
},
},
}
);

const User = model('User', schema);

const answer = User.myStaticMethod(); // 42
```
6 changes: 3 additions & 3 deletions lib/schema.js
Expand Up @@ -107,11 +107,11 @@ function Schema(obj, options) {
this.inherits = {};
this.callQueue = [];
this._indexes = [];
this.methods = {};
this.methods = (options && options.methods) || {};
this.methodOptions = {};
this.statics = {};
this.statics = (options && options.statics) || {};
this.tree = {};
this.query = {};
this.query = (options && options.query) || {};
this.childSchemas = [];
this.plugins = [];
// For internal debugging. Do not use this to try to save a schema in MDB.
Expand Down
9 changes: 9 additions & 0 deletions test/document.test.js
Expand Up @@ -11455,3 +11455,12 @@ describe('document', function() {
assert.ok(baz.populated('bar'));
});
});

describe('Check if instance function that is supplied in schema option is availabe', function() {
it('should give an instance function back rather than undefined', function ModelJS() {
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');
});
});
7 changes: 7 additions & 0 deletions test/model.test.js
Expand Up @@ -8505,6 +8505,13 @@ describe('Model', function() {
});
});

describe('Check if static function that is supplied in schema option is available', function() {
it('should give a static function back rather than undefined', function ModelJS() {
const testSchema = new mongoose.Schema({}, { statics: { staticFn() { return 'Returned from staticFn'; } } });
const TestModel = mongoose.model('TestModel', testSchema);
assert.equal(TestModel.staticFn(), 'Returned from staticFn');
});
});



Expand Down
30 changes: 30 additions & 0 deletions test/query.test.js
Expand Up @@ -3962,6 +3962,36 @@ describe('Query', function() {
assert.equal(result.length, 1);
assert.equal(result[0].name, '@foo.com');
});

it('should return query helper supplied in schema options query property instead of undefined', function(done) {
const Model = db.model('Test', new Schema({
userName: {
type: String,
required: [true, 'userName is required']
}
}, {
query: {
byUserName(userName) {
return this.where({ userName });
}
}
}));

Model.create({ userName: 'test' }, function(error) {
if (error instanceof Error) {
return done(error);
}
Model.find().byUserName('test').exec(function(error, docs) {
if (error instanceof Error) {
return done(error);
}
assert.equal(docs.length, 1);
assert.equal(docs[0].userName, 'test');
done();
});
});
});

it('allows a transform option for lean on a query gh-10423', async function() {
const arraySchema = new mongoose.Schema({
sub: String
Expand Down

0 comments on commit f48eee2

Please sign in to comment.