模型与模型关系
We have added authentication in our application in the previous post, you maybe have a question, can I add some fields to Post
document and remember the user who created it and the one who updated it at the last time.
我们在上一篇文章的应用程序中添加了身份验证,您可能有一个问题,我可以在Post
文档中添加一些字段,并记住创建它的用户和上次更新它的用户。
When I come to the Post
model, and try to add fields to setup the auditors, I can not find a simple way to do this. After researching and consulting from the Nestjs channel in Discord, I was told that the @nestjs/mongoose
can not deal with the relations between Documents.
当我进入Post
模型并尝试添加字段来设置审核员时,我找不到一种简单的方法来执行此操作。 在Discord的Nestjs频道进行研究和咨询后,我被告知@nestjs/mongoose
无法处理文档之间的关系。
There are some suggestions I got from the community.
我从社区中得到了一些建议。
Use Typegoose instead of
@nestjs/mongoose
, check the typegoose doc for more details. More effectively, there is a nestjs-typegoose to assist you to bridge typegoose to the Nestjs world.使用Typegoose代替
@nestjs/mongoose
,查看typegoose文档以获取更多详细信息。 更有效的是,有一个nestjs-typegoose可以帮助您将typegoose桥接到Nestjs世界。Give up
@nestjs/mongoose
and turn back to use the rawmongoose
APIs instead.放弃
@nestjs/mongoose
,然后转回使用原始的mongoose
API。
I have some experience of express and mongoose written in legacy ES5, so in this post I will try to switch to use the pure Mongoose API to replace the modeling codes we have done in the previous post. With the help of @types/mongoose
, it is easy to apply static types on the mongoose schemas , documents and models.
我有一些用旧版ES5编写的Express和Mongoose的经验,因此在这篇文章中,我将尝试切换为使用纯Mongoose API代替我们在上一篇文章中所做的建模代码。 借助@types/mongoose
,很容易将静态类型应用于mongoose架构,文档和模型。
使用Mongoose API重新定义模型 (Redefining the models with Mongoose API)
We will follow the following steps to clean the codes of models one by one .
我们将按照以下步骤一步一步地清理模型代码。
- Clean the document definition interface. 清洁文档定义界面。
- Redefine the schema for related documents using Mongoose APIs. 使用Mongoose API重新定义相关文档的架构。
- Define mongoose Models and provide them in the Nestjs IOC engine. 定义猫鼬模型,并将其提供给Nestjs IOC引擎。
- Create a custom provider for connecting to Mongo using Mongoose APIs. 创建一个自定义提供程序以使用Mongoose API连接到Mongo。
Remove the
@nestjs/mongoose
dependency finally.最后删除
@nestjs/mongoose
依赖项。
Firstly let’s have a look at Post
, in the post.model.ts
, fill the following content:
首先让我们看一下Post
,在post.model.ts
,填写以下内容:
import { Document, Schema, SchemaTypes } from 'mongoose';
import { User } from './user.model';export interface Post extends Document {
readonly title: string;
readonly content: string;
readonly createdBy?: Partial<User>;
readonly updatedBy?: Partial<User>;
}export const PostSchema = new Schema(
{
title: SchemaTypes.String,
content: SchemaTypes.String,
createdBy: { type: SchemaTypes.ObjectId, ref: 'User', required: false },
updatedBy: { type: SchemaTypes.ObjectId, ref: 'User', required: false },
},
{ timestamps: true },
);
The PostSchema
is defined by type-safe way, all supports can be found in SchemeTypes
while navigating it. The createdBy
and updatedBy
is a reference of User
document. The { timestamps: true }
will append createdAt
and updatedAt
to the document and fill these two fields the current timestamp automatically when saving and updating the documents.
PostSchema
是通过类型安全的方式定义的,在导航时,可以在SchemeTypes
找到所有支持。 的createdBy
和updatedBy
是的参考User
文档。 该{ timestamps: true }
将追加createdAt
和updatedAt
到文档并保存和更新文档时自动填充这两个领域当前的时间戳。
Create a database.providers.ts
file to declare the Post
model. We also create a provider for Mongo connection.
创建一个database.providers.ts
文件来声明Post
模型。 我们还为Mongo连接创建了一个提供程序。
import { PostSchema, Post } from './post.model';
import {
DATABASE_CONNECTION,
POST_MODEL
} from './database.constants';export const databaseProviders = [
{
provide: DATABASE_CONNECTION,
useFactory: (): Promise<typeof mongoose> =>
connect('mongodb://localhost/blog', {
useNewUrlParser: true,
useUnifiedTopology: true,
}),
},
{
provide: POST_MODEL,
useFactory: (connection: Connection) =>
connection.model<Post>('Post', PostSchema, 'posts'),
inject: [DATABASE_CONNECTION],
},
//...
];
More info about creating custom providers, check the custom providers chapter of the official docs.
有关创建自定义提供程序的更多信息,请查看官方文档的“自定义提供程序”一章。
For the convenience of using the injection token, create a database.constant.ts
file to define series of constants for further uses.
为了方便使用注入令牌,请创建一个database.constant.ts
文件来定义一系列常量以供进一步使用。
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
export const POST_MODEL = 'POST_MODEL';
export const USER_MODEL = 'USER_MODEL';
export const COMMENT_MODEL = 'COMMENT_MODEL';
Create a database.module.ts
file, and define a Module
to collect the Mongoose related resources.
创建一个database.module.ts
文件,并定义一个Module
来收集猫鼬相关的资源。
@Module({
providers: [...databaseProviders],
exports: [...databaseProviders],
})
export class DatabaseModule {}
To better organize the codes, move all model related codes into the database
folder.
为了更好地组织代码,请将所有与模型相关的代码移动到database
文件夹中。
Import DatabaseModule
in the AppModule
.
在AppModule
导入DatabaseModule
。
@Module({
imports: [
DatabaseModule,
//...
})
export class AppModule {}
Now in the post.service.ts
, change the injecting Model<Post>
to the following.
现在在post.service.ts
,将注入Model<Post>
更改为以下内容。
constructor(
@Inject(POST_MODEL) private postModel: Model<Post>,
//...
){...}
In the test, change the injection token from class name to the constant value we defined, eg.
在测试中,将注入令牌从类名更改为我们定义的常量值,例如。
module.get<Model<Post>>(POST_MODEL)
Similarly, update the user.model.ts
and related codes.
同样,更新user.model.ts
和相关代码。
//database/user.model.ts
export interface User extends Document {
readonly username: string;
readonly email: string;
readonly password: string;
readonly firstName?: string;
readonly lastName?: string;
readonly roles?: RoleType[];
}const UserSchema = new Schema(
{
username: SchemaTypes.String,
password: SchemaTypes.String,
email: SchemaTypes.String,
firstName: { type: SchemaTypes.String, required: false },
lastName: { type: SchemaTypes.String, required: false },
roles: [
{ type: SchemaTypes.String, enum: ['ADMIN', 'USER'], required: false },
],
// createdAt: { type: SchemaTypes.Date, required: false },
// updatedAt: { type: SchemaTypes.Date, required: false },
},
{ timestamps: true },
);UserSchema.virtual('name').get(function() {
return `${this.firstName} ${this.lastName}`;
});export const userModelFn = (conn: Connection) =>
conn.model<User>('User', UserSchema, 'users');
//database/role-type.enum.ts
export enum RoleType {
ADMIN = 'ADMIN',
USER = 'USER',
}//database/database.providers.ts
export const databaseProviders = [
//...
{
provide: USER_MODEL,
useFactory: (connection: Connection) => userModelFn(connection),
inject: [DATABASE_CONNECTION],
},
];//user/user.service.ts
@Injectable()
export class UserService {
constructor(@Inject(USER_MODEL) private userModel: Model<User>) {}
//...
}
Create another model Comment
, as sub document of Post
. A comment holds a reference of Post doc.
创建另一个模型Comment
,作为Post
子文档。 评论包含Post doc的参考。
export interface Comment extends Document {
readonly content: string;
readonly post?: Partial<Post>;
readonly createdBy?: Partial<User>;
readonly updatedBy?: Partial<User>;
}export const CommentSchema = new Schema(
{
content: SchemaTypes.String,
post: { type: SchemaTypes.ObjectId, ref: 'Post', required: false },
createdBy: { type: SchemaTypes.ObjectId, ref: 'User', required: false },
updatedBy: { type: SchemaTypes.ObjectId, ref: 'User', required: false },
},
{ timestamps: true },
);
Register it in databaseProviders
.
在databaseProviders
注册它。
export const databaseProviders = [
//...
{
provide: COMMENT_MODEL,
useFactory: (connection: Connection) =>
connection.model<Post>('Comment', CommentSchema, 'comments'),
inject: [DATABASE_CONNECTION],
},
]
Update the PostService
, add two methods.
更新PostService
,添加两个方法。
//post/post.service.ts
export class PostService {
constructor(
@Inject(POST_MODEL) private postModel: Model<Post>,
@Inject(COMMENT_MODEL) private commentModel: Model<Comment>
) {}
//...
// actions for comments
createCommentFor(id: string, data: CreateCommentDto): Observable<Comment> {
const createdComment = this.commentModel.create({
post: { _id: id },
...data,
createdBy: { _id: this.req.user._id },
});
return from(createdComment);
} commentsOf(id: string): Observable<Comment[]> {
const comments = this.commentModel
.find({
post: { _id: id },
})
.select('-post')
.exec();
return from(comments);
}
}
The CreateCommentDto
is a POJO to collect the data from request body.
CreateCommentDto
是一个POJO,用于从请求正文中收集数据。
//post/create-comment.dto.ts
export class CreateCommentDto {
readonly content: string;
}
Open PostController
, add two methods.
打开PostController
,添加两个方法。
export class PostController {
constructor(private postService: PostService) {} //...
@Post(':id/comments')
createCommentForPost(
@Param('id') id: string,
@Body() data: CreateCommentDto,
): Observable<Comment> {
return this.postService.createCommentFor(id, data);
} @Get(':id/comments')
getAllCommentsOfPost(@Param('id') id: string): Observable<Comment[]> {
return this.postService.commentsOf(id);
}
}
In the last post, we created authentication, to protect the saving and updating operations, you can set JwtGuard
on the methods of the controllers.
在上JwtGuard
文章中,我们创建了身份验证,以保护保存和更新操作,您可以在控制器的方法上设置JwtGuard
。
But if we want to control the access in details, we need to consider Authorization
, most of time, it is simple to implement it by introducing RBAC.
但是,如果要详细控制访问,则需要考虑Authorization
,大多数情况下,通过引入RBAC即可轻松实现。
基于角色的访问控制 (Role based access control)
Assume there are two roles defined in this application, USER
and ADMIN
. In fact, we have already defined an enum class to archive this purpose.
假设在此应用程序中定义了两个角色: USER
和ADMIN
。 实际上,我们已经定义了一个枚举类来存档此目的。
Nestjs provide a simple way to set metadata by decorator on methods.
Nestjs提供了一种通过装饰器在方法上设置元数据的简单方法。
import { SetMetadata } from '@nestjs/common';
import { RoleType } from '../database/role-type.enum';
import { HAS_ROLES_KEY } from './auth.constants';export const HasRoles = (...args: RoleType[]) => SetMetadata(HAS_ROLES_KEY, args);
Create specific Guard
to read the metadata and compare the user object in request and decide if allow user to access the controlled resources.
创建特定的Guard
以读取元数据并比较请求中的用户对象,并确定是否允许用户访问受控资源。
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const roles = this.reflector.get<RoleType[]>(
HAS_ROLES_KEY,
context.getHandler(),
);
if (!roles) {
return true;
} const { user }= context.switchToHttp().getRequest() as AuthenticatedRequest;
return user.roles && user.roles.some(r => roles.includes(r));
}
}
For example, we require a USER
role to create a Post
document.
例如,我们需要一个USER
角色来创建一个Post
文档。
export class PostController {
constructor(private postService: PostService) {}
@Post('')
@UseGuards(JwtAuthGuard, RolesGuard)
@HasRoles(RoleType.USER, RoleType.ADMIN)
createPost(@Body() post: CreatePostDto): Observable<BlogPost> {
//...
}
}
You can add other rules on the resource access, such as a USER
role is required to update a Post
, and ADMIN
is to delete a Post
.
您可以在资源访问上添加其他规则,例如,需要USER
角色来更新Post
,而ADMIN
则是删除Post
。
添加审核信息 (Adding auditing info)
We have added roles to control access the resources, now we can save the current user who is creating the post or update the post.
我们添加了角色来控制对资源的访问,现在我们可以保存正在创建帖子的当前用户或更新帖子。
There is a barrier when we wan to read the authenticated user from request and set it to fields createdBy
and updatedBy
in PostService
, the PostService
is singleton scoped, you can not inject a request in it. But you can declare the PostService
is REQUEST
scoped, thus injecting a request instance is possible.
有一个障碍,当我们婉读取请求的已验证的用户,并将其设置为场createdBy
和updatedBy
在PostService
的PostService
是单范围的,你不能注入中有一个请求。 但是您可以声明PostService
为REQUEST
范围,因此可以注入请求实例。
@Injectable({ scope: Scope.REQUEST })
export class PostService {
constructor(
@Inject(POST_MODEL) private postModel: Model<Post>,
@Inject(COMMENT_MODEL) private commentModel: Model<Comment>,
@Inject(REQUEST) private req: AuthenticatedRequest,
) {}
//...
save(data: CreatePostDto): Observable<Post> {
const createPost = this.postModel.create({
...data,
createdBy: { _id: this.req.user._id },
});
return from(createPost);
}
update(id: string, data: UpdatePostDto): Observable<Post> {
return from(
this.postModel
.findOneAndUpdate(
{ _id: id },
{ ...data, updatedBy: { _id: this.req.user._id } },
)
.exec(),
);
}
// actions for comments
createCommentFor(id: string, data: CreateCommentDto): Observable<Comment> {
const createdComment = this.commentModel.create({
post: { _id: id },
...data,
createdBy: { _id: this.req.user._id },
});
return from(createdComment);
}
}
As a convention in Nestjs, you have to make PostController
available in the REQUEST
scoped.
按照Nestjs的约定,您必须使PostController
在REQUEST
范围内可用。
@Controller({path:'posts', scope:Scope.REQUEST})
export class PostController {...}
In the test codes, you have to resolve
to replace get
to get the instance from Nestjs test harness.
在测试代码中,您必须resolve
替换get
以从Nestjs测试工具中获取实例。
describe('Post Controller', () => {
describe('Replace PostService in provider(useClass: PostServiceStub)', () => {
let controller: PostController; beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: PostService,
useClass: PostServiceStub,
},
],
controllers: [PostController],
}).compile(); controller = await module.resolve<PostController>(PostController);// use resovle here....
});
...
PostService
also should be changed to request scoped.
PostService
也应更改为请求范围。
@Injectable({ scope: Scope.REQUEST })
export class PostService {...}
In the post.service.spec.ts
, you have to update the mocking progress.
在post.service.spec.ts
,您必须更新post.service.spec.ts
进度。
describe('PostService', () => {
let service: PostService;
let model: Model<Post>;
let commentModel: Model<Comment>; beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PostService,
{
provide: POST_MODEL,
useValue: {
new: jest.fn(),
constructor: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
create: jest.fn(),
remove: jest.fn(),
exec: jest.fn(),
deleteMany: jest.fn(),
deleteOne: jest.fn(),
updateOne: jest.fn(),
findOneAndUpdate: jest.fn(),
findOneAndDelete: jest.fn(),
},
},
{
provide: COMMENT_MODEL,
useValue: {
new: jest.fn(),
constructor: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
updateOne: jest.fn(),
deleteOne: jest.fn(),
update: jest.fn(),
create: jest.fn(),
remove: jest.fn(),
exec: jest.fn(),
},
},
{
provide: REQUEST,
useValue: {
user: {
id: 'dummyId',
},
},
},
],
}).compile(); service = await module.resolve<PostService>(PostService);
model = module.get<Model<Post>>(POST_MODEL);
commentModel = module.get<Model<Comment>>(COMMENT_MODEL);
});
//...
运行应用程序 (Run the application)
Now we have done the clean work, run the application to make sure it works as expected.
现在,我们已经完成了清理工作,运行该应用程序以确保它能够按预期工作。
> npm run start
Use curl
to test the endpoints provided in the application.
使用curl
测试应用程序中提供的端点。
$ curl http://localhost:3000/auth/login -d "{\"username\":\"hantsy\",\"password\":\"password\"}" -H "Content-Type:application/json" {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cG4iOiJoYW50c3kiLCJzdWIiOiI1ZWYwYjdkNTRkMDY3MzIxMTQxODQ1ZjYiLCJlbWFpbCI6ImhhbnRzeUBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlVTRVIiXSwiaWF0IjoxNTkyODM0MDE3LCJleHAiOjE1OTI4Mzc2MTd9.Jx53KIWHgyPADhLr-LhjW-iu1e8hD650e9nduGgJ8Bw"}$ curl -X POST http://localhost:3000/posts -d "{\"title\":\"my title\",\"content\":\"my content\"}" -H "Content-Type:application/json" -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cG4iOiJoYW50c3kiLCJzdWIiOiI1ZWYwYjdkNTRkMDY3MzIxMTQxODQ1ZjYiLCJlbWFpbCI6ImhhbnRzeUBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlVTRVIiXSwiaWF0IjoxNTkyODM0MDE3LCJleHAiOjE1OTI4Mzc2MTd9.Jx53KIWHgyPADhLr-LhjW-iu1e8hD650e9nduGgJ8Bw"{"_id":"5ef0b7fe4d067321141845fc","title":"my title","content":"my content","createdBy":"5ef0b7d54d067321141845f6","createdAt":"2020-06-22T13:54:06.873Z","updatedAt":"2020-06-22T13:54:06.873Z","__v":0}$ curl -X POST http://localhost:3000/posts/5ef0b7fe4d067321141845fc/comments -d "{\"content\":\"my content\"}" -H "Content-Type:application/json" -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cG4iOiJoYW50c3kiLCJzdWIiOiI1ZWYwYjdkNTRkMDY3MzIxMTQxODQ1ZjYiLCJlbWFpbCI6ImhhbnRzeUBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlVTRVIiXSwiaWF0IjoxNTkyODM0MDE3LCJleHAiOjE1OTI4Mzc2MTd9.Jx53KIWHgyPADhLr-LhjW-iu1e8hD650e9nduGgJ8Bw"{"_id":"5ef0b8414d067321141845fd","post":"5ef0b7fe4d067321141845fc","content":"my content","createdBy":"5ef0b7d54d067321141845f6","createdAt":"2020-06-22T13:55:13.822Z","updatedAt":"2020-06-22T13:55:13.822Z","__v":0}$ curl http://localhost:3000/posts/5ef0b7fe4d067321141845fc/comments
[{"_id":"5ef0b8414d067321141845fd","content":"my content","createdBy":"5ef0b7d54d067321141845f6","createdAt":"2020-06-22T13:55:13.822Z","updatedAt":"2020-06-22T13:55:13.822Z","__v":0}]
最后一件事 (One last thing)
After cleaning up the codes, we do not need the @nestjs/mongoose
dependency, let's remove it.
清理代码后,我们不需要@nestjs/mongoose
依赖项,让我们删除它。
npm uninstall --save @nestjs/mongoose
Grab the source codes from my github, switch to branch feat/model.
从我的github获取源代码,切换到branch feat / model 。
翻译自: https://medium.com/@hantsy/dealing-with-model-relations-cf0993e0ba88
模型与模型关系