zenstack-plugin-dev
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseZenStack V3 — Plugin Development
ZenStack V3 — 插件开发
Plugins are a preview feature and may change in future releases.
A ZenStack plugin can extend the toolkit at three levels, independently or together:
- Schema (ZModel) — contribute custom attributes (e.g. ) and functions to the schema language.
@password - CLI (code generation) — participate in to emit artifacts from the schema.
zen generate - Runtime (ORM) — install on the client to intercept queries, hook CRUD lifecycle events, transform SQL, add custom methods, or compute fields.
ZenStack's replaces Prisma's . To declare/consume plugins in a schema see
; this skill is about authoring them.
plugingeneratorzenstack-schema-modelingPackages: (runtime plugin types), (CLI plugin types + ZModel
AST). Install the SDK as a dev dependency: .
@zenstackhq/orm@zenstackhq/sdknpm install -D @zenstackhq/sdk插件是一项 预览 功能,未来版本中可能会发生变化。
ZenStack 插件可以在三个层级扩展工具集,这些层级可以独立使用,也可以组合使用:
- Schema(ZModel)——向 schema 语言贡献自定义 attribute(例如 )和 function。
@password - CLI(代码生成)——参与 ,根据 schema 生成产物。
zen generate - Runtime(ORM)——安装到 client 上,以拦截查询、挂钩 CRUD 生命周期事件、转换 SQL、添加自定义方法或计算字段。
ZenStack 的 取代 Prisma 的 。若要在 schema 中声明/使用 plugin,请参阅 ;本 skill 重点介绍如何编写这些 plugin。
plugingeneratorzenstack-schema-modelingPackages:(runtime plugin 类型)、(CLI plugin 类型 + ZModel AST)。将 SDK 作为 dev dependency 安装:。
@zenstackhq/orm@zenstackhq/sdknpm install -D @zenstackhq/sdkDeclaring a plugin in ZModel
在 ZModel 中声明 plugin
zmodel
plugin myPlugin {
provider = './my-plugin' // local .ts/.js/.mts/.mjs file, a folder with an index, or an npm package
output = './generated' // arbitrary key=value options, readable by the plugin
}provider@core/typescript@core/prismazmodel
plugin myPlugin {
provider = './my-plugin' // local .ts/.js/.mts/.mjs file, a folder with an index, or an npm package
output = './generated' // arbitrary key=value options, readable by the plugin
}provider@core/typescript@core/prismaRuntime plugins
Runtime plugin
A runtime plugin is an object implementing from . Define it three
ways; gives the best type inference:
RuntimePlugin@zenstackhq/ormdefinePlugints
import { definePlugin, type RuntimePlugin } from '@zenstackhq/orm';
// plain object / definePlugin (recommended)
const plugin = definePlugin({ id: 'my-plugin', /* hooks… */ });
// or a class
class MyPlugin implements RuntimePlugin<typeof schema> {
id = 'my-plugin';
/* hooks… */
}Install on the client (immutable — returns a new client, original unchanged); remove with
/ . You can also pass to the constructor.
$unuse(id)$unuseAll()plugins: [...]ZenStackClientts
const db = new ZenStackClient(schema, { dialect });
const withPlugin = db.$use(plugin); // new client
const plain = withPlugin.$unuse('my-plugin'); // remove by id
// or: new ZenStackClient(schema, { dialect, plugins: [plugin] });Every plugin needs a unique . Add any combination of the hooks below.
idruntime plugin 是一个实现 中 的对象。共有三种定义方式; 可提供最佳类型推断:
@zenstackhq/ormRuntimePlugindefinePlugints
import { definePlugin, type RuntimePlugin } from '@zenstackhq/orm';
// plain object / definePlugin (recommended)
const plugin = definePlugin({ id: 'my-plugin', /* hooks… */ });
// or a class
class MyPlugin implements RuntimePlugin<typeof schema> {
id = 'my-plugin';
/* hooks… */
}在 client 上安装(不可变——返回一个全新的 client,原 client 保持不变);使用 / 移除。也可以向 constructor 传入 。
$unuse(id)$unuseAll()ZenStackClientplugins: [...]ts
const db = new ZenStackClient(schema, { dialect });
const withPlugin = db.$use(plugin); // new client
const plain = withPlugin.$unuse('my-plugin'); // remove by id
// or: new ZenStackClient(schema, { dialect, plugins: [plugin] });每个 plugin 都需要唯一的 。可以添加下列 hook 的任意组合。
idonQuery
— intercept ORM operations
onQueryonQuery
——拦截 ORM 操作
onQueryFires for ORM API calls only (, , …), not the query builder. Wrap, modify
args, short-circuit, or post-process results via .
findManycreateproceedts
definePlugin({
id: 'timing',
onQuery: async ({ model, operation, args, proceed }) => {
const start = Date.now();
const result = await proceed(args);
console.log(`${model}.${operation} took ${Date.now() - start}ms`);
return result;
},
});仅在 ORM API 调用(、 等)时触发,不作用于 query builder。通过 包装、修改参数、短路,或对结果进行后处理。
findManycreateproceedts
definePlugin({
id: 'timing',
onQuery: async ({ model, operation, args, proceed }) => {
const start = Date.now();
const result = await proceed(args);
console.log(`${model}.${operation} took ${Date.now() - start}ms`);
return result;
},
});onKyselyQuery
— intercept/transform SQL
onKyselyQueryonKyselyQuery
——拦截/转换 SQL
onKyselyQueryThe lowest-level hook: every DB access (ORM and query builder) flows through Kysely here, so you
can inspect or rewrite the SQL AST () before . Good for soft-deletes,
injected filters, column transforms.
OperationNodeproceedts
import type { OnKyselyQueryCallback } from '@zenstackhq/orm';
const hook: OnKyselyQueryCallback<typeof schema> = async ({ query, proceed }) => {
// inspect/transform `query` (a Kysely OperationNode), e.g. with an OperationNodeTransformer
return proceed(query);
};
definePlugin({ id: 'sql-rewrite', onKyselyQuery: hook });最低层级的 hook:每次数据库访问(ORM 以及 query builder)都会流经 Kysely,因此你可以在 之前检查或重写 SQL AST()。适用于软删除、注入过滤条件和列转换。
proceedOperationNodets
import type { OnKyselyQueryCallback } from '@zenstackhq/orm';
const hook: OnKyselyQueryCallback<typeof schema> = async ({ query, proceed }) => {
// inspect/transform `query` (a Kysely OperationNode), e.g. with an OperationNodeTransformer
return proceed(query);
};
definePlugin({ id: 'sql-rewrite', onKyselyQuery: hook });onEntityMutation
— before/after create·update·delete
onEntityMutationonEntityMutation
——create·update·delete 之前/之后
onEntityMutationLifecycle hooks around entity mutations, with lazy loaders for the affected rows.
ts
definePlugin({
id: 'audit',
onEntityMutation: {
runAfterMutationWithinTransaction: false, // default: run after the tx commits
async beforeEntityMutation({ model, action, loadBeforeMutationEntities }) {
// action: 'create' | 'update' | 'delete'
if (model === 'Post' && action === 'update') {
const before = await loadBeforeMutationEntities();
}
},
async afterEntityMutation({ model, action, loadAfterMutationEntities }) {
const after = await loadAfterMutationEntities();
},
},
});Set to run the after-hook atomically inside the mutation's
transaction. Note: mutations triggered by DB cascades are not captured, and the loaders can be
expensive on bulk mutations.
runAfterMutationWithinTransaction: true围绕实体 mutation 的生命周期 hook,并提供用于加载受影响行的 lazy loader。
ts
definePlugin({
id: 'audit',
onEntityMutation: {
runAfterMutationWithinTransaction: false, // default: run after the tx commits
async beforeEntityMutation({ model, action, loadBeforeMutationEntities }) {
// action: 'create' | 'update' | 'delete'
if (model === 'Post' && action === 'update') {
const before = await loadBeforeMutationEntities();
}
},
async afterEntityMutation({ model, action, loadAfterMutationEntities }) {
const after = await loadAfterMutationEntities();
},
},
});设置 可让 after-hook 在 mutation 的 transaction 内以原子方式运行。注意:由数据库 cascade 触发的 mutation 不会被捕获,而且这些 loader 在批量 mutation 中可能开销较大。
runAfterMutationWithinTransaction: trueExtending the client (client
)
client扩展 client(client
)
clientAdd custom methods/properties under . Names must be -prefixed so they don't shadow
model accessors; grouping under one object is recommended.
client$$namespacets
const cacheDb = db.$use(definePlugin({
id: 'cache',
client: {
$cache: {
get stats() { return { hits: 0, misses: 0 }; },
async invalidate() { /* … */ },
},
},
}));
await cacheDb.$cache.invalidate();在 下添加自定义方法/属性。名称必须以 为前缀,以免遮蔽 model accessor;建议将它们统一分组到一个 对象下。
client$$namespacets
const cacheDb = db.$use(definePlugin({
id: 'cache',
client: {
$cache: {
get stats() { return { hits: 0, misses: 0 }; },
async invalidate() { /* … */ },
},
},
}));
await cacheDb.$cache.invalidate();Custom query args (queryArgs
)
queryArgs自定义 query args(queryArgs
)
queryArgsAccept extra options on ORM calls, validated with a Zod schema. Target , the
/// groups, or specific operations.
$all$create$read$update$deletets
import { z } from 'zod';
definePlugin({
id: 'cache',
queryArgs: {
$read: z.object({ cache: z.strictObject({ ttl: z.number().positive().optional() }).optional() }),
},
onQuery: ({ args, proceed }) => proceed(args), // read (args as any).cache
});
// await db.user.findMany({ where: {...}, cache: { ttl: 60 } });允许 ORM 调用接受额外选项,并使用 Zod schema 进行验证。可针对 、/// 组或特定 operation。
$all$create$read$update$deletets
import { z } from 'zod';
definePlugin({
id: 'cache',
queryArgs: {
$read: z.object({ cache: z.strictObject({ ttl: z.number().positive().optional() }).optional() }),
},
onQuery: ({ args, proceed }) => proceed(args), // read (args as any).cache
});
// await db.user.findMany({ where: {...}, cache: { ttl: 60 } });Computed result fields (result
, v3.5.0+)
result计算结果字段(result
,v3.5.0+)
resultAdd virtual fields computed after a query from declared fields.
needsts
definePlugin({
id: 'full-name',
result: {
user: {
fullName: {
needs: { firstName: true, lastName: true },
compute: (user) => `${user.firstName} ${user.lastName}`,
},
},
},
});添加虚拟字段,在查询完成后依据声明的 字段进行计算。
needsts
definePlugin({
id: 'full-name',
result: {
user: {
fullName: {
needs: { firstName: true, lastName: true },
compute: (user) => `${user.firstName} ${user.lastName}`,
},
},
},
});CLI plugins (code generation)
CLI plugin(代码生成)
A CLI plugin is an ESM module whose default export is a with a callback.
It runs during , receiving the parsed ZModel AST.
CliPlugingeneratezen generatets
import type { CliPlugin } from '@zenstackhq/sdk';
import { isDataModel, type InvocationExpr } from '@zenstackhq/sdk/ast';
import fs from 'node:fs';
const plugin: CliPlugin = {
name: 'Password Report',
// statusText is optional
generate: ({ model, defaultOutputPath, pluginOptions }) => {
if (pluginOptions['report'] !== true) return; // options come from the ZModel plugin block
let out = '# Password Fields\n\n';
for (const dm of model.declarations.filter(isDataModel)) {
for (const field of dm.fields) {
const attr = field.attributes.find((a) => a.decl.$refText === '@password');
if (attr) out += `- ${dm.name}.${field.name}\n`;
}
}
fs.writeFileSync(`${defaultOutputPath}/password-fields.md`, out, 'utf-8');
},
};
export default plugin;generate{ model, defaultOutputPath, pluginOptions }outputpluginCLI plugin 是一个 ESM module,其默认导出为带 callback 的 。它会在 期间运行,并接收已解析的 ZModel AST。
generateCliPluginzen generatets
import type { CliPlugin } from '@zenstackhq/sdk';
import { isDataModel, type InvocationExpr } from '@zenstackhq/sdk/ast';
import fs from 'node:fs';
const plugin: CliPlugin = {
name: 'Password Report',
// statusText is optional
generate: ({ model, defaultOutputPath, pluginOptions }) => {
if (pluginOptions['report'] !== true) return; // options come from the ZModel plugin block
let out = '# Password Fields\n\n';
for (const dm of model.declarations.filter(isDataModel)) {
for (const field of dm.fields) {
const attr = field.attributes.find((a) => a.decl.$refText === '@password');
if (attr) out += `- ${dm.name}.${field.name}\n`;
}
}
fs.writeFileSync(`${defaultOutputPath}/password-fields.md`, out, 'utf-8');
},
};
export default plugin;generate{ model, defaultOutputPath, pluginOptions }outputpluginSchema plugins (custom attributes & functions)
Schema plugin(自定义 attribute 与 function)
Declare new ZModel attributes/functions in a the plugin ships, then use them in the app
schema. constrains where an attribute applies.
.zmodel@@@targetFieldzmodel
// my-plugin/plugin.zmodel
attribute @password(hasher: Any) @@@targetField([StringField])
function bcryptHasher(rounds: Int): Any {}zmodel
// app schema
model User {
id Int @id @default(autoincrement())
password String @password(hasher: bcryptHasher(10))
}在 plugin 附带的 文件中声明新的 ZModel attribute/function,然后在 app schema 中使用。 用于约束 attribute 的适用范围。
.zmodel@@@targetFieldzmodel
// my-plugin/plugin.zmodel
attribute @password(hasher: Any) @@@targetField([StringField])
function bcryptHasher(rounds: Int): Any {}zmodel
// app schema
model User {
id Int @id @default(autoincrement())
password String @password(hasher: bcryptHasher(10))
}End-to-end pattern: password hasher
端到端模式:password hasher
The docs' worked example combines all three levels into one plugin package:
- Schema — defines
plugin.zmodel+@password; the app schema appliesbcryptHasher()to a field.@password(hasher: bcryptHasher(10)) - CLI — a walks the AST and emits a "password fields" report when the
CliPlugin.generateoption is set.report - Runtime — a with
RuntimePluginuses a KyselyonKyselyQueryto detectOperationNodeTransformerfields (via@password) and hash values onschema.models[...].fields[...].attributes/INSERTbefore they hit the database.UPDATE
Install with (or the constructor option), then
and to run the CLI side.
db.$use(new PasswordHasherPlugin())pluginsnpm install -D @zenstackhq/sdkzen generateUse Zod v4 forschemas to match ZenStack's bundled version.queryArgs
文档中的实战示例将三个层级组合到一个 plugin package 中:
- Schema——定义
plugin.zmodel+@password;app schema 将bcryptHasher()应用到某个字段。@password(hasher: bcryptHasher(10)) - CLI——一个 遍历 AST,当设置
CliPlugin.generate选项时生成“password fields”报告。report - Runtime——一个 使用
RuntimePlugin和 KyselyonKyselyQuery检测OperationNodeTransformer字段(通过@password),并在值写入 database 前对schema.models[...].fields[...].attributes/INSERT的值进行哈希处理。UPDATE
使用 安装(或使用 constructor option),然后执行 和 以运行 CLI 部分。
db.$use(new PasswordHasherPlugin())pluginsnpm install -D @zenstackhq/sdkzen generateschema 请使用 Zod v4,以匹配 ZenStack 捆绑的版本。queryArgs
Reference docs
参考文档
Full ZenStack documentation for this topic is bundled under :
references/- plugin-dev.md — end-to-end plugin development guide
- runtime-plugins-overview.md — runtime plugin model
- extending-orm-client.md — client methods, query args, computed fields
- entity-mutation-hooks.md — before/after mutation hooks
- kysely-query-hooks.md — SQL transformation
onKyselyQuery - query-api-hooks.md — ORM interception
onQuery - zmodel-plugin.md — declaring plugins in ZModel
- zmodel-plugin-reference.md — block reference
plugin
本主题的完整 ZenStack 文档打包在 下:
references/- plugin-dev.md——端到端 plugin 开发指南
- runtime-plugins-overview.md——runtime plugin 模型
- extending-orm-client.md——client 方法、query args 和计算字段
- entity-mutation-hooks.md——变更前后 hook
- kysely-query-hooks.md——SQL 转换
onKyselyQuery - query-api-hooks.md——ORM 拦截
onQuery - zmodel-plugin.md——在 ZModel 中声明 plugin
- zmodel-plugin-reference.md——块参考
plugin