Skip to content

Commit

Permalink
Merge pull request #12507 from Automattic/vkarpov15/gh-12342
Browse files Browse the repository at this point in the history
TypeScript query helpers quick fix and docs improvements
  • Loading branch information
vkarpov15 committed Oct 3, 2022
2 parents ff7eed5 + 1f95170 commit 4b79461
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 31 deletions.
70 changes: 40 additions & 30 deletions docs/typescript/query-helpers.md
Expand Up @@ -3,6 +3,8 @@
[Query helpers](http://thecodebarbarian.com/mongoose-custom-query-methods.html) let you define custom helper methods on Mongoose queries.
Query helpers make queries more semantic using chaining syntax.

The following is an example of how query helpers work in JavaScript.

```javascript
ProjectSchema.query.byName = function(name) {
return this.find({ name: name });
Expand All @@ -14,9 +16,9 @@ var Project = mongoose.model('Project', ProjectSchema);
Project.find().where('stars').gt(1000).byName('mongoose');
```

In TypeScript, Mongoose does support manually typed and automatically typed Query Helpers.
## Manually Typed Query Helpers

1- Manually typed:
In TypeScript, you can define query helpers using a separate query helpers interface.
Mongoose's `Model` takes 3 generic parameters:

1. The `DocType`
Expand All @@ -30,23 +32,34 @@ Below is an example of creating a `ProjectModel` with a `byName` query helper.
import { HydratedDocument, Model, Query, Schema, model } from 'mongoose';

interface Project {
name: string;
stars: number;
name?: string;
stars?: number;
}

type ProjectModelType = Model<Project, ProjectQueryHelpers>;
// Query helpers should return `Query<any, Document<DocType>> & ProjectQueryHelpers`
// to enable chaining.
type ProjectModelQuery = Query<any, HydratedDocument<Project>, ProjectQueryHelpers> & ProjectQueryHelpers;
interface ProjectQueryHelpers {
byName(this: ProjectModelQuery, name: string): ProjectModelQuery;
byName(name: string): QueryWithHelpers<
HydratedDocument<Project>[],
HydratedDocument<Project>,
ProjectQueryHelpers
>
}

const schema = new Schema<Project, ProjectModelType, {}, ProjectQueryHelpers>({
name: { type: String, required: true },
stars: { type: Number, required: true }
type ProjectModelType = Model<Project, ProjectQueryHelpers>;

const ProjectSchema = new Schema<
Project,
Model<Project, ProjectQueryHelpers>,
{},
ProjectQueryHelpers
>({
name: String,
stars: Number
});
schema.query.byName = function(name: string): ProjectModelQuery {

ProjectSchema.query.byName = function byName(
this: QueryWithHelpers<any, HydratedDocument<Project>, ProjectQueryHelpers>,
name: string
) {
return this.find({ name: name });
};

Expand All @@ -63,30 +76,27 @@ async function run(): Promise<void> {
}
```

2- Automatically typed:
## Auto Typed Query Helpers

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 ProjectSchema = new Schema({
name: String,
stars: Number
}, {
query: {
byName(name: string) {
return this.find({ name });
}
});
}
});

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

// Equivalent to `ProjectModel.find({ stars: { $gt: 1000 }, name: 'mongoose' })`
await ProjectModel.find().where('stars').gt(1000).byName('mongoose');
}
// Equivalent to `ProjectModel.find({ stars: { $gt: 1000 }, name: 'mongoose' })`
await ProjectModel.find().where('stars').gt(1000).byName('mongoose');
```
63 changes: 63 additions & 0 deletions test/types/queries.test.ts
Expand Up @@ -359,3 +359,66 @@ function gh12142() {
}
);
}

async function gh12342_manual() {
interface Project {
name?: string, stars?: number
}

interface ProjectQueryHelpers {
byName(name: string): QueryWithHelpers<
HydratedDocument<Project>[],
HydratedDocument<Project>,
ProjectQueryHelpers
>
}

type ProjectModelType = Model<Project, ProjectQueryHelpers>;

const ProjectSchema = new Schema<
Project,
Model<Project, ProjectQueryHelpers>,
{},
ProjectQueryHelpers
>({
name: String,
stars: Number
});

ProjectSchema.query.byName = function byName(
this: QueryWithHelpers<any, HydratedDocument<Project>, ProjectQueryHelpers>,
name: string
) {
return this.find({ name: name });
};

// 2nd param to `model()` is the Model class to return.
const ProjectModel = model<Project, ProjectModelType>('Project', schema);

expectType<HydratedDocument<Project>[]>(
await ProjectModel.findOne().where('stars').gt(1000).byName('mongoose')
);
}

async function gh12342_auto() {
interface Project {
name?: string, stars?: number
}

const ProjectSchema = new Schema({
name: String,
stars: Number
}, {
query: {
byName(name: string) {
return this.find({ name });
}
}
});

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

expectType<HydratedDocument<Project>[]>(
await ProjectModel.findOne().where('stars').gt(1000).byName('mongoose')
);
}
2 changes: 1 addition & 1 deletion types/schemaoptions.d.ts
Expand Up @@ -201,7 +201,7 @@ declare module 'mongoose' {
/**
* Query helper functions.
*/
query?: Record<any, <T extends QueryWithHelpers<unknown, DocType>>(this: T, ...args: any) => T> | QueryHelpers,
query?: Record<any, <T extends QueryWithHelpers<unknown, HydratedDocument<DocType>>>(this: T, ...args: any) => T> | QueryHelpers,

/**
* Set whether to cast non-array values to arrays.
Expand Down

0 comments on commit 4b79461

Please sign in to comment.