zenstack-plugin-dev

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ZenStack 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:
  1. Schema (ZModel) — contribute custom attributes (e.g.
    @password
    ) and functions to the schema language.
  2. CLI (code generation) — participate in
    zen generate
    to emit artifacts from the schema.
  3. Runtime (ORM) — install on the client to intercept queries, hook CRUD lifecycle events, transform SQL, add custom methods, or compute fields.
ZenStack's
plugin
replaces Prisma's
generator
. To declare/consume plugins in a schema see
zenstack-schema-modeling
; this skill is about authoring them.
Packages:
@zenstackhq/orm
(runtime plugin types),
@zenstackhq/sdk
(CLI plugin types + ZModel AST). Install the SDK as a dev dependency:
npm install -D @zenstackhq/sdk
.
插件是一项 预览 功能,未来版本中可能会发生变化。
ZenStack 插件可以在三个层级扩展工具集,这些层级可以独立使用,也可以组合使用:
  1. Schema(ZModel)——向 schema 语言贡献自定义 attribute(例如
    @password
    )和 function。
  2. CLI(代码生成)——参与
    zen generate
    ,根据 schema 生成产物。
  3. Runtime(ORM)——安装到 client 上,以拦截查询、挂钩 CRUD 生命周期事件、转换 SQL、添加自定义方法或计算字段。
ZenStack 的
plugin
取代 Prisma 的
generator
。若要在 schema 中声明/使用 plugin,请参阅
zenstack-schema-modeling
;本 skill 重点介绍如何编写这些 plugin。
Packages:
@zenstackhq/orm
(runtime plugin 类型)、
@zenstackhq/sdk
(CLI plugin 类型 + ZModel AST)。将 SDK 作为 dev dependency 安装:
npm install -D @zenstackhq/sdk
。

Declaring 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
resolution order: local file path → folder with an index → npm package. CLI plugins must be ESM; TypeScript providers are loaded via jiti. The built-in
@core/typescript
plugin runs automatically;
@core/prisma
emits a Prisma schema.
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
的解析顺序:本地文件路径 → 包含 index 的文件夹 → npm package。CLI plugin 必须使用 ESM;TypeScript provider 通过 jiti 加载。内置的
@core/typescript
plugin 会自动运行;
@core/prisma
用于生成 Prisma schema。

Runtime plugins

Runtime plugin

A runtime plugin is an object implementing
RuntimePlugin
from
@zenstackhq/orm
. Define it three ways;
definePlugin
gives the best type inference:
ts
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
$unuse(id)
/
$unuseAll()
. You can also pass
plugins: [...]
to the
ZenStackClient
constructor.
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] });
Every plugin needs a unique
id
. Add any combination of the hooks below.
runtime plugin 是一个实现
@zenstackhq/orm
中
RuntimePlugin
的对象。共有三种定义方式;
definePlugin
可提供最佳类型推断:
ts
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 保持不变);使用
$unuse(id)
/
$unuseAll()
移除。也可以向
ZenStackClient
constructor 传入
plugins: [...]
。
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 都需要唯一的
id
。可以添加下列 hook 的任意组合。

onQuery
— intercept ORM operations

onQuery
——拦截 ORM 操作

Fires for ORM API calls only (
findMany
,
create
, …), not the query builder. Wrap, modify args, short-circuit, or post-process results via
proceed
.
ts
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 调用(
findMany
、
create
等)时触发,不作用于 query builder。通过
proceed
包装、修改参数、短路,或对结果进行后处理。
ts
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

onKyselyQuery
——拦截/转换 SQL

The lowest-level hook: every DB access (ORM and query builder) flows through Kysely here, so you can inspect or rewrite the SQL AST (
OperationNode
) before
proceed
. Good for soft-deletes, injected filters, column transforms.
ts
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,因此你可以在
proceed
之前检查或重写 SQL AST(
OperationNode
)。适用于软删除、注入过滤条件和列转换。
ts
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

onEntityMutation
——create·update·delete 之前/之后

Lifecycle 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
runAfterMutationWithinTransaction: true
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.
围绕实体 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();
        },
    },
});
设置
runAfterMutationWithinTransaction: true
可让 after-hook 在 mutation 的 transaction 内以原子方式运行。注意:由数据库 cascade 触发的 mutation 不会被捕获,而且这些 loader 在批量 mutation 中可能开销较大。

Extending the client (
client
)

扩展 client(
client
)

Add custom methods/properties under
client
. Names must be
$
-prefixed
so they don't shadow model accessors; grouping under one
$namespace
object is recommended.
ts
const cacheDb = db.$use(definePlugin({
    id: 'cache',
    client: {
        $cache: {
            get stats() { return { hits: 0, misses: 0 }; },
            async invalidate() { /* … */ },
        },
    },
}));
await cacheDb.$cache.invalidate();
在
client
下添加自定义方法/属性。名称必须以
$
为前缀,以免遮蔽 model accessor;建议将它们统一分组到一个
$namespace
对象下。
ts
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
)

自定义 query args(
queryArgs
)

Accept extra options on ORM calls, validated with a Zod schema. Target
$all
, the
$create
/
$read
/
$update
/
$delete
groups, or specific operations.
ts
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 进行验证。可针对
$all
、
$create
/
$read
/
$update
/
$delete
组或特定 operation。
ts
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
,v3.5.0+)

Add virtual fields computed after a query from declared
needs
fields.
ts
definePlugin({
    id: 'full-name',
    result: {
        user: {
            fullName: {
                needs: { firstName: true, lastName: true },
                compute: (user) => `${user.firstName} ${user.lastName}`,
            },
        },
    },
});
添加虚拟字段,在查询完成后依据声明的
needs
字段进行计算。
ts
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
CliPlugin
with a
generate
callback. It runs during
zen generate
, receiving the parsed ZModel AST.
ts
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
receives
{ model, defaultOutputPath, pluginOptions }
— the ZModel AST, the resolved output directory (the
output
option or a default), and the options from the
plugin
block.
CLI plugin 是一个 ESM module,其默认导出为带
generate
callback 的
CliPlugin
。它会在
zen generate
期间运行,并接收已解析的 ZModel AST。
ts
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 }
——ZModel AST、解析后的输出目录(
output
选项或默认值),以及
plugin
块中的选项。

Schema plugins (custom attributes & functions)

Schema plugin(自定义 attribute 与 function)

Declare new ZModel attributes/functions in a
.zmodel
the plugin ships, then use them in the app schema.
@@@targetField
constrains where an attribute applies.
zmodel
// 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
文件中声明新的 ZModel attribute/function,然后在 app schema 中使用。
@@@targetField
用于约束 attribute 的适用范围。
zmodel
// 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:
  1. Schema —
    plugin.zmodel
    defines
    @password
    +
    bcryptHasher()
    ; the app schema applies
    @password(hasher: bcryptHasher(10))
    to a field.
  2. CLI — a
    CliPlugin.generate
    walks the AST and emits a "password fields" report when the
    report
    option is set.
  3. Runtime — a
    RuntimePlugin
    with
    onKyselyQuery
    uses a Kysely
    OperationNodeTransformer
    to detect
    @password
    fields (via
    schema.models[...].fields[...].attributes
    ) and hash values on
    INSERT
    /
    UPDATE
    before they hit the database.
Install with
db.$use(new PasswordHasherPlugin())
(or the
plugins
constructor option), then
npm install -D @zenstackhq/sdk
and
zen generate
to run the CLI side.
Use Zod v4 for
queryArgs
schemas to match ZenStack's bundled version.
文档中的实战示例将三个层级组合到一个 plugin package 中:
  1. Schema——
    plugin.zmodel
    定义
    @password
    +
    bcryptHasher()
    ;app schema 将
    @password(hasher: bcryptHasher(10))
    应用到某个字段。
  2. CLI——一个
    CliPlugin.generate
    遍历 AST,当设置
    report
    选项时生成“password fields”报告。
  3. Runtime——一个
    RuntimePlugin
    使用
    onKyselyQuery
    和 Kysely
    OperationNodeTransformer
    检测
    @password
    字段(通过
    schema.models[...].fields[...].attributes
    ),并在值写入 database 前对
    INSERT
    /
    UPDATE
    的值进行哈希处理。
使用
db.$use(new PasswordHasherPlugin())
安装(或使用
plugins
constructor option),然后执行
npm install -D @zenstackhq/sdk
和
zen generate
以运行 CLI 部分。
queryArgs
schema 请使用 Zod v4,以匹配 ZenStack 捆绑的版本。

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 —
    onKyselyQuery
    SQL transformation
  • query-api-hooks.md —
    onQuery
    ORM interception
  • zmodel-plugin.md — declaring plugins in ZModel
  • zmodel-plugin-reference.md —
    plugin
    block reference
本主题的完整 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——
    onKyselyQuery
    SQL 转换
  • query-api-hooks.md——
    onQuery
    ORM 拦截
  • zmodel-plugin.md——在 ZModel 中声明 plugin
  • zmodel-plugin-reference.md——
    plugin
    块参考