模型与模型关系_处理模型关系

模型与模型关系

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 raw mongoose 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 .

我们将按照以下步骤一步一步地清理模型代码。

  1. Clean the document definition interface.

    清洁文档定义界面。
  2. Redefine the schema for related documents using Mongoose APIs.

    使用Mongoose API重新定义相关文档的架构。
  3. Define mongoose Models and provide them in the Nestjs IOC engine.

    定义猫鼬模型,并将其提供给Nestjs IOC引擎。
  4. Create a custom provider for connecting to Mongo using Mongoose APIs.

    创建一个自定义提供程序以使用Mongoose API连接到Mongo。
  5. 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找到所有支持。 的createdByupdatedBy是的参考User文档。 该{ timestamps: true }将追加createdAtupdatedAt到文档并保存和更新文档时自动填充这两个领域当前的时间戳。

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.

假设在此应用程序中定义了两个角色: USERADMIN 。 实际上,我们已经定义了一个枚举类来存档此目的。

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.

有一个障碍,当我们婉读取请求的已验证的用户,并将其设置为场createdByupdatedByPostServicePostService是单范围的,你不能注入中有一个请求。 但是您可以声明PostServiceREQUEST范围,因此可以注入请求实例。

@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的约定,您必须使PostControllerREQUEST范围内可用。

@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

模型与模型关系

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值