1、简介
。ZenStack 是一个构建在 Prisma 之上的开源工具包 - 最流行的 Node.js ORM。ZenStack 将 Prisma 的功能提升到一个新的水平,并提高了堆栈每一层的开发效率 - 从访问控制到 API 开发,一直到前端。
2、主要特点
● 具有内置访问控制、数据验证、多态关系等的 ORM
● 自动生成的CRUD API - RESTful & tRPC
● 自动生成的 OpenAPI 文档
● 自动生成的前端数据查询钩子 - SWR & TanStack查询
● 与流行的身份验证服务和全栈/后端框架集成
● 具有出色可扩展性的插件系统
3、增强的ORM
ZenStack 所做的最基本的事情是将 Prisma 变成一个更强大的数据库工具,而不仅仅是一个 ORM:
● 访问策略
● 数据验证
● 自定义属性和函数
● 多文件架构
● 模型继承
● 多态关系
● …
从本质上讲,您的 schema 成为数据和规则的单一事实来源,在运行时,您的数据库客户端不仅处理 CRUD 操作,而且还以安全的方式执行。
基于prisma
Prisma 是一种所谓的“架构优先”ORM,它简化了 Node.js 和 TypeScript 应用程序的数据库访问。它为定义数据模型提供了直观简洁的 DSL(域特定语言),并生成了用于访问数据库的类型安全客户端。
扩展为ZModel
与其他 Prisma 工具不同,我们创建了一种名为 ZModel 的新模式语言。它是 Prisma 架构语言 (PSL) 的超集,具有语法元素以支持其他功能。CLI 将 ZModel 文件作为输入,并从中生成 Prisma 架构文件 - 反过来,该文件可以馈送到标准 CLI 以生成 Prisma 客户端或迁移数据库。所有 Prisma 架构语法在 ZModel 中都有效。
增强的prisma客户端
ZenStack 最重点部分。
增强的 Prisma Client 是包装常规 Prisma Client 实例的透明代理。它具有与原始 Prisma 客户端相同的 API,但通过拦截 API 调用来添加其他行为。添加的行为包括:
● 实施访问策略
● 数据验证
● 哈希密码
● 在查询结果中省略字段
未来还会有更多。
创建增强的 Prisma 客户端很容易,只需使用常规 Prisma 客户端调用 API 即可:enhance
import { PrismaClient } from '@prisma/client';
import { enhance } from '@zenstackhq/runtime';
const prisma = new PrismaClient();
const db = enhance(prisma);
// db has the same typing as prisma
await db.user.findMany();
await db.user.create({ data: { email: '[email protected]'} });
● 它不会导致建立新的数据库连接。通常为每个请求创建一个新的增强型 Prisma 客户端。
● 可以将 Prisma 客户端扩展与增强客户端结合使用
● 可以将原始客户端和增强客户端一起使用
使增强的 Prisma 客户端与原始 Prisma 客户端尽可能兼容,但仍存在一些限制:
无顺序操作事务
增强的 Prisma CLient 不支持顺序操作事务。请改用交互式交易,或者只使用原始的 Prisma 客户端。
4、访问策略
模型级策略
最常用的访问策略是在模型级别声明的策略 - 使用 和 模型级别属性编写。@@allow@@deny
操作:readupdatedeleteall
条件:布尔表达式
model Post {
id Int @id
title String
published Boolean @default(false)
@@allow('read', true)
@@deny('read', !published)
}
ZenStack 使用以下逻辑判断是否允许 CRUD 操作:
- 如果任何规则的计算结果为 true,则拒绝该规则。@@deny
- 如果任何规则的计算结果为 true,则允许该规则。@@allow
- 否则,它将被拒绝(默认为安全)。
每种 CRUD 操作类型都控制一组 Prisma Client 方法,如下所示:
● 创造
create、、 和 // 嵌套在 create/update 调用中。createManyupsertcreatecreateManyconnectOrCreate
● 读
findMany和。findUniquefindUniqueOrThrowfindFirstfindFirstOrThrowcountaggregategroupBy
“read” 操作还确定是否可以读取 和 method 返回的值。createupdatedelete
● 更新
update、、 、 和 / 嵌套在 create/update 调用中。updateManyupsertupdateupdateManysetconnectconnectOrCreatedisconnect
● 删除
delete、 和 嵌套在 update 调用中。deleteManydelete
策略条件中可以使用模型参数,如
model User {
...
@@allow('read', startsWith(email, 'joey'))
@@allow('update', auth().name == annotated_by || annotated_by=="ai")
}
运行是过滤运行
仅返回满足策略的行。 仅对满足策略的行进行计数。这也适用于嵌套读取。ZenStack 通过将子句注入 Prisma 查询来执行“读取”策略。
数据权限过滤
身份验证和授权
认证
身份验证是验证用户身份的过程。它可以是简单的电子邮件和密码验证,也可以是与第三方身份提供商交互的 OAuth 工作流程。
ZenStack 不是身份验证解决方案,但它通常依赖于一个才能工作。在实际应用程序中,访问控制规则通常需要利用请求用户的信息。身份验证解决方案提供用户信息。
ZenStack 与您使用的身份验证解决方案无关。身份验证系统只需要能够提供代表当前用户的对象。ZenStack 使用它来创建用户范围的增强型 Prisma 客户端,以强制执行访问策略。
身份验证解决方案:
● Auth.js (formerly NextAuth)
● Clerk
● Supabase Auth
● Auth0
● Firebase Authentication
● Lucia
授权
授权是确定用户是否有权执行特定操作的过程。ZenStack 的核心部分是直接与数据库耦合的授权解决方案。它控制用户可以对表或字段执行哪些 CRUD 操作。
下面列出了一些常用的建模授权模式(灵活性和复杂性越来越高):
- 访问控制列表 (ACL)
用户直接分配权限。例如,可以向用户授予对表的权限。readPost - 基于角色的访问控制 (RBAC)
为用户分配角色,并为角色配置权限。例如,可以为用户分配一个角色,该角色被授予对表的完全权限。adminPost - 基于属性的访问控制 (ABAC)
权限是根据用户和资源的属性定义的。例如,如果已发布行并且用户是订阅成员,则可以为用户分配对行的“读取”权限。PostPost - 基于关系的访问控制 (ReBAC)
权限由主题和资源之间是否存在的关系定义。例如,如果用户与 .PostPost
复杂的应用程序通常需要使用这些模式的组合。ZenStack 的访问策略系统旨在易于使用,同时具有出色的灵活性来应对这些挑战。
访问当前用户
大多数情况下,是否允许 CRUD 操作取决于当前用户。ZenStack 在策略规则中提供了一个特殊的功能来访问当前用户。auth()
model User {
id Int @id
role String
posts Post[]
}
model Post {
id Int @id
title String
author User @relation(fields: [authorId], references: [id])
authorId Int
// ✅ valid rule
@@allow('all', auth().role == 'ADMIN')
// ❌ invalid rule, `subscribed` field doesn't exist in `User` model
@@allow('all', auth().subscribed == true)
}
您可以通过向其添加属性来使用未命名为 auth 模型的模型。User@@auth
model MyUser {
id Int @id
role String
posts Post[]
@@auth
}
如果我不在数据库中存储用户怎么办?
可以像模型一样包含字段,但它没有映射到数据库表,仅用于引入。User
model User {
id String @id
name String
@@ignore
}
提供当前用户
由于 ZenStack 不是身份验证解决方案,因此它不知道当前用户是谁。由开发人员从身份验证端获取它并将其传递给 ZenStack(在调用函数时)。enhance
// `getCurrentUser` is an authentication API that extracts
// the current user from the request
const user = await getCurrentUser(request);
// create an enhanced Prisma Client for the user, the `user` object
// provides value for the `auth()` function in policy rules
const db = enhance(prisma, { user })
对象的最低要求是具有身份验证模型的“id”字段的值(如果模型使用复合 id 字段,则需要分配所有字段)。但是,如果您的访问策略涉及身份验证模型的其他字段,则还需要提供这些字段。user
ZenStack 不会自动查询数据库来获取缺失的字段,但您可以根据需要执行此操作。假设您有如下策略规则:
@@allow('update', auth().role == 'ADMIN')
在调用 之前,您可以点击数据库(使用原始 Prisma 客户端)来获取用户的角色:enhance()
const userId = getCurrentUserId(request);
const user = await prisma.user.findUniqueOrThrow({ where: { id: userId }, select: { id: true, role: true } });
const db = enhance(prisma, { user });
将来,我们可能会引入一个新选项来控制是否应自动从数据库中获取缺失的字段。
使用auth()
您可以通过在调用 时不传递对象(或传递 )来指示当前用户是匿名的 :userundefinedenhance
const db = enhance(prisma);
在策略规则中,检查匿名用户:auth() == null
model Post {
...
// allow all login users to read
@@allow('read', auth() != null)
}
与其他字段比较auth()
您可以与相同类型的字段(即 auth 模型)进行比较。这种比较相当于 id 字段比较。例如,以下策略规则:auth()
model Post {
...
author User @relation(fields: [authorId], references: [id])
authorId Int
@@allow('update', auth() == author)
}
等效于:
model Post {
...
author User @relation(fields: [authorId], references: [id])
authorId Int
@@allow('update', auth().id == author.id)
}
,也等效于:
model Post {
...
author User @relation(fields: [authorId], references: [id])
authorId Int
@@allow('update', auth().id == authorId)
}
使用关联字段授权验证
一对一关联关系,只需直接引用该字段即可。您可以使用点表示法进一步访问关系的字段。
model List {
id Int
private Boolean
}
model Todo {
id Int
list List @relation(...)
// `list` references a to-one relation
@@allow('update', !list.private)
}
调用表达式
用于调用函数。例如,是一个 Invocation Expression。auth()
一元表达式
● !逻辑 NOT,操作数必须为布尔值
二进制表达式
● ==相等性,在比较模型类型时转换为 id 比较
● !=不等式,在比较模型类型时转换为 id 比较
● >大于,两个操作数都必须是数字
● >=大于或等于,两个操作数都必须是数字
● <小于,两个操作数都必须是 number
● <=小于或等于,两个操作数都必须是数字
● &&逻辑 AND,两个操作数都必须是布尔值
● ||逻辑 OR,两个操作数都必须是布尔值
● in成员身份,其中左操作数是数组,右操作数是文本或对枚举字段的引用
字段级策略
我们介绍了如何编写模型级策略来控制 CRUD 权限。在数据库术语中,这称为行级安全性。借助关系遍历、当前用户访问以及灵活的表达式和函数的强大功能,您可以完全有能力处理大多数实际授权场景。但是,有时您会发现自己需要更精细的访问控制。
ZenStack 的字段级策略允许您为单个字段定义访问规则。例如,您可以允许博客文章的所有者更新其标题和内容,但只有具有“EDITOR”角色的用户才能更改“published”字段。
模型级和字段级策略的组合为您提供了终极的粒度和灵活性,并让 ZenStack 超越了 Postgres 的原生行级安全功能。
定义字段级策略
要定义字段级策略,请使用 和 字段级属性将规则附加到字段。请注意,字段级属性始终以单个 .下面是一个示例:@allow@deny
model Post {
...
published Boolean @allow('update', auth().role == EDITOR)
}
● 字段级策略仅支持 “read”、“update” 和 “all” 操作。在字段级别控制 “create” 和 “delete” 权限没有意义。
● 默认情况下,允许字段级访问。如果您未将任何规则附加到字段,则只要模型可访问,就可以访问该字段。相反,默认情况下会拒绝模型级访问。您必须使用 attributes 显式开放访问权限。@@allow
数据验证
如下
model User {
id Int @id
email String @unique @email @endsWith('@zenstack.dev')
imgUrl String? @url
password String @length(min: 8, max: 32)
age Int @gt(0) @lt(120)
}
5、自动CRUD API
API 设计是一个复杂的话题。在考虑将 CRUD API 包装在 ORM 周围时,我们认为有两个主要的竞争目标:
● 查询灵活性
Prisma 的 API 非常灵活,尤其是在嵌套读取和写入方面。最好在派生的 API 中完全保留这种灵活性。这样,您还可以将 Prisma 的使用知识转移到使用 API 上。
● RESTfulness
在设计 CRUD API 时,将其设为 RESTful 是一个自然的选择。面向资源的 URL 和语义 HTTP 动词与问题非常匹配。虽然 RESTful API 传统上存在 N + 1 问题(传统 ORM 也是如此),但我们可以使用一些约定来缓解它。