Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Infer schema type automatically. #11563

Merged
merged 42 commits into from
Jun 5, 2022
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
2dbe288
Add TS utilities to infer schema type automatically.
mohammad0-0ahmad Mar 24, 2022
03e34db
Import inferschematype in index.d.ts file & refactor Schema class.
mohammad0-0ahmad Mar 24, 2022
ec6eb3d
Refactor model FN type & Model interface type.
mohammad0-0ahmad Mar 24, 2022
9321fd5
Make schema type obtainable from schema its constructor parameter "de…
mohammad0-0ahmad Mar 24, 2022
2145afa
Supporting custom typeKey to infer schema type.
mohammad0-0ahmad Mar 24, 2022
34ae74c
Add statics property in schema options to supporting auto typed model…
mohammad0-0ahmad Mar 24, 2022
967bd65
Add methods property in schema options to supporting auto typed docum…
mohammad0-0ahmad Mar 24, 2022
e75d4b6
Improve create FN type to show actual schema type instead of any
mohammad0-0ahmad Mar 24, 2022
ff2bf3e
Improve ApplyBasicQueryCasting type to improve find FN suggestions
mohammad0-0ahmad Mar 24, 2022
1bf940e
Refact create FN returned type
mohammad0-0ahmad Mar 24, 2022
4a5d712
Refact Model new FN & some related tests.
mohammad0-0ahmad Mar 26, 2022
295bad2
Fix typo from the previous PR
mohammad0-0ahmad Mar 26, 2022
cd215a7
Fix typo
mohammad0-0ahmad Mar 26, 2022
a7967d9
Improve Model.insertMany FN type
mohammad0-0ahmad Mar 27, 2022
e9d4491
Add query property in schema options to support auto typed query help…
mohammad0-0ahmad Mar 28, 2022
526ff6a
Rearrang Schema class generics
mohammad0-0ahmad Mar 28, 2022
411350b
Rearrange SchemaOptions generics.
mohammad0-0ahmad Mar 28, 2022
b942ce8
refactor query type in SchemaOptions & related tests.
mohammad0-0ahmad Mar 29, 2022
6bcb7a4
refactor statics & methods properties in SchemaOptions to get better …
mohammad0-0ahmad Mar 29, 2022
56af40b
Pass DocType to statics, methods & query props in SchemaOptions to ge…
mohammad0-0ahmad Mar 29, 2022
9cdec80
Refactor statics & methods props in SchemaOptions.
mohammad0-0ahmad Mar 29, 2022
b3e9668
Add ability to infer path of mixed array type.
mohammad0-0ahmad Apr 4, 2022
a42bbde
Refactor the previous PR.
mohammad0-0ahmad Apr 4, 2022
302de56
Refactor some function names & comment lines.
mohammad0-0ahmad Apr 5, 2022
96571b1
Refactor test in query.test.js file.
mohammad0-0ahmad Apr 5, 2022
897452b
Resolve Mixed type as any type.
mohammad0-0ahmad Apr 19, 2022
503c0d1
Refactor doc.populate FN to make it accept any Model type.
mohammad0-0ahmad Apr 22, 2022
aa6497d
Refactor Model new FN
mohammad0-0ahmad Apr 28, 2022
a3b32ac
Refactor model FN to optimize TS server performance.
mohammad0-0ahmad Apr 29, 2022
569d4eb
create MergeBOntoA And implement instead of UnpackedIntersection in s…
mohammad0-0ahmad Apr 29, 2022
0fe4928
Add some details about auto typed schema.
mohammad0-0ahmad Apr 29, 2022
f4f17d7
Add some details about auto typed statics functions
mohammad0-0ahmad Apr 29, 2022
6ef2f6c
Add some details about auto typed query helpers.
mohammad0-0ahmad Apr 29, 2022
6a77f09
Add details about auto typed statics, query-helpers and instance meth…
mohammad0-0ahmad Apr 30, 2022
f21dfbe
Refactor docs.
mohammad0-0ahmad Apr 30, 2022
7093195
Undo auto docs formatting changes.
mohammad0-0ahmad May 1, 2022
555db64
Refactor docs & some comments in tests files.
mohammad0-0ahmad May 3, 2022
0131b7e
fix typo
mohammad0-0ahmad May 17, 2022
47469d6
Cover decimal128
mohammad0-0ahmad Jun 2, 2022
e0d299f
patch autotyping to handle properly population and
Uzlopak Jun 2, 2022
e137a6d
Fix linting
mohammad0-0ahmad Jun 3, 2022
6547fe2
Merge branch '6.4' into auto-typed-schema
vkarpov15 Jun 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions docs/guide.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -3962,4 +3962,34 @@ 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();
});
});
});

});
Loading