6-9 更新Permission与Role更新Policy逻辑
本节完成 Permission 与 Role 模块的 Policy 关联逻辑,涵盖 Create(创建时关联 Policy)和 Update(更新时先删后建)两个核心操作,涉及 Prisma 事务、嵌套写入(Nested Writes)、DTO 序列化等关键技术点。
一、Permission 创建时关联 Policy
1. CreatePermissionDto 添加嵌套验证
与创建 Role 时不同,Permission 的 DTO 需要使用 @ValidateNested 装饰器来验证嵌套的 Policy 数组:
// dto/create-permission.dto.ts
import { IsString, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { CreatePolicyDto } from './create-policy.dto';
export class CreatePermissionDto {
@IsString()
name: string;
@IsString()
action: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreatePolicyDto)
policies: CreatePolicyDto[];
}
typescript
@ValidateNested 确保 policies 数组中的每个对象都会按照 CreatePolicyDto 的规则进行深度验证。
2. Policy Service 添加 encode 字段
Policy 的 encode 字段由 type、effect、action、subject 等属性拼接后进行 Base64 编码生成,作为唯一标识:
// policy.service.ts
async create(dto: CreatePolicyDto) {
const encode = Buffer.from(
`${dto.type}:${dto.effect}:${dto.action}:${dto.subject}`
).toString('base64');
const data = { ...dto, encode };
return this.prisma.policy.create({ data });
}
typescript
encode 的作用:当用户端只传递了
type、effect、action、subject等字段而没有id时,encode作为 where 条件用于判断该 Policy 是否已存在于数据库中。这在给角色添加权限的场景中非常实用——前端展示系统拥有的 subject 及其 fields,用户填写type、effect、action、conditions、fields等信息,后端通过encode匹配已有记录。
3. Controller 事务创建
使用 Prisma 交互式事务(Interactive Transaction)完成 Permission 和 Policy 的关联创建:
// permission.controller.ts
@Post()
async create(@Body() dto: CreatePermissionDto) {
return this.prisma.$transaction(async (prisma) => {
const { policies, ...permissionData } = dto;
const permission = await prisma.permission.create({
data: permissionData,
});
for (const policy of policies) {
const encode = Buffer.from(
`${policy.type}:${policy.effect}:${policy.action}:${policy.subject}`
).toString('base64');
await prisma.permissionPolicy.create({
data: {
permission: { connect: { id: permission.id } },
policy: {
connectOrCreate: {
where: { encode },
create: { ...policy, encode },
},
},
},
});
}
return permission;
});
}
typescript
4. 测试验证
请求体示例:
{
"name": "permissions_read",
"action": "read",
"policies": [
{
"type": "1",
"effect": "allow",
"action": "read",
"subject": "Permission"
},
{
"type": "2",
"effect": "allow",
"action": "read",
"subject": "Role"
}
]
}
json
数据库验证结果:
permissions表新增一条记录(id=20)permission_policies表关联了两条 policy(id=3, id=4)policies表通过connectOrCreate自动创建或关联
二、Permission 更新时重建 Policy 关联
1. 更新逻辑的核心:先删后建
更新 Permission 的 Policy 关联时,采用 deleteMany + create 策略:先删除旧的关联关系,再根据传入数据创建新关联。
// permission.controller.ts
@Patch(':id')
async update(
@Param('id') id: string,
@Body() dto: UpdatePermissionDto,
) {
const { policies, ...permissionData } = dto;
return this.prisma.$transaction(async (prisma) => {
const updated = await prisma.permission.update({
where: { id: +id },
data: {
...permissionData,
permissionPolicies: {
deleteMany: {}, // 1. 删除所有旧的关联关系
create: policies?.map((policy) => {
const encode = Buffer.from(
`${policy.type}:${policy.effect}:${policy.action}:${policy.subject}`
).toString('base64');
return {
policy: {
connectOrCreate: {
where: { encode }, // 2. 通过 encode 查找已有 Policy
create: { ...policy, encode }, // 3. 不存在则创建
},
},
};
}) || [],
},
},
include: {
permissionPolicies: {
include: { policy: true },
},
},
});
return updated;
});
}
typescript
关键步骤解析:
| 步骤 | 操作 | 说明 |
|---|---|---|
deleteMany: {} | 删除 permission_policies 表中的关联记录 | 空条件 = 删除该 Permission 的所有关联 |
connectOrCreate.where | 通过 encode 查找 Policy | 支持两种匹配条件:id 或 encode |
connectOrCreate.create | 创建新 Policy | 不存在时自动创建 |
include | 返回更新后的完整数据 | 包含关联的 Policy 详细信息 |
2. UpdatePermissionDto 设计
// dto/update-permission.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreatePermissionDto } from './create-permission.dto';
import { IsOptional, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { CreatePolicyDto } from './create-policy.dto';
export class UpdatePermissionDto extends PartialType(CreatePermissionDto) {
@IsOptional()
@ValidateNested({ each: true })
@Type(() => CreatePolicyDto)
policies?: CreatePolicyDto[];
}
typescript
3. 响应序列化 DTO
使用 @Expose 和 @Transform 装饰器处理响应数据的格式转换,移除 encode 字段并重命名嵌套属性:
// dto/public-update-permission-detail.dto.ts
import { Exclude, Expose, Type, plainToInstance } from 'class-transformer';
import { CreatePolicyDto } from './create-policy.dto';
export class PublicUpdatePermissionDetailDto {
@Expose({ name: 'permissionPolicies' })
@Type(() => Object)
@Transform(({ value }) =>
(value || []).map((item: any) => {
const policy = item.policy;
delete policy.encode;
delete item.policy;
item.policies = policy;
return item;
}),
)
policies: CreatePolicyDto[];
}
typescript
在 Controller 上应用序列化:
@SerializeOptions({ excludeExtraneousValues: true })
@UseInterceptors(ClassSerializerInterceptor)
@Patch(':id')
async update(...) { ... }
typescript
三、数据库验证流程
更新操作后,在 Prisma Studio 中验证数据一致性:
- permissions 表:记录本身不变
- permission_policies 表:旧的关联已被清除,只保留新的关联
- policies 表:通过
connectOrCreate匹配已有记录或创建新记录
四、Role Policy 更新(作业)
Role 的 Policy 更新逻辑与 Permission 完全对称,核心代码结构相同:
// role.controller.ts
@Patch(':id')
async update(@Param('id') id: string, @Body() dto: UpdateRoleDto) {
return this.prisma.$transaction(async (prisma) => {
const { policies, ...roleData } = dto;
return prisma.role.update({
where: { id: +id },
data: {
...roleData,
rolePolicies: {
deleteMany: {},
create: policies?.map((policy) => ({
policy: {
connectOrCreate: {
where: { id: policy.id }, // 挑战:仅传 ID 时的处理
create: { ...policy, encode: generateEncode(policy) },
},
},
})) || [],
},
},
include: { rolePolicies: { include: { policy: true } } },
});
});
}
typescript
挑战任务:当请求体中只传递了 Policy 的 id 而没有 type、effect、action、subject 等字段时,如何完成更新?需要解决两个问题:
- DTO 验证:使用
@IsOptional装饰器让其他字段在提供了id时变为可选 - connectOrCreate 处理:当没有 Policy 的完整数据时,仅通过
id进行connect操作
// 条件判断示例
connectOrCreate: policy.id && !policy.type
? { where: { id: policy.id }, create: {} } // 仅 ID 时直接 connect
: {
where: { encode: generateEncode(policy) },
create: { ...policy, encode: generateEncode(policy) },
}
typescript
五、关键概念对比
| 概念 | 说明 | 适用场景 |
|---|---|---|
connectOrCreate | 查找已有记录或创建新记录 | Policy 可能被多个 Permission/Role 共用 |
deleteMany: {} | 删除关联表中的全部关联记录 | 更新时先清除旧关联 |
encode (Base64) | Policy 唯一标识 | 无 ID 时判断记录是否已存在 |
$transaction | Prisma 交互式事务 | 确保关联操作的原子性 |
@ValidateNested | 嵌套对象验证 | DTO 中包含数组类型的子 DTO |
六、总结
本节完成了 Permission 和 Role 与 Policy 的 CRUD 关联操作,核心模式为:
创建: connectOrCreate → 自动匹配或创建 Policy
更新: deleteMany → 先清除旧关联 → connectOrCreate → 重建新关联
序列化: @Transform → 移除 encode → 重命名嵌套属性
text
这一模式可复用于任何需要管理多对多关联关系的业务场景。
↑