mcpField
Z
zenstack-plugin-dev

zenstack-plugin-dev

github.com/zenstackhq开发3439 次安装2 天前更新
代码仓库

关于 zenstack-plugin-dev

zenstack-plugin-dev 是 ZenStack V3 插件开发技能,介绍如何扩展 ZModel、`zen generate` 代码生成和 ORM 运行时。适合需要编写自定义属性、生成器或查询钩子的开发者;插件功能目前为预览版。

主要功能

  1. 01扩展 ZModel 属性与函数
  2. 02为 CLI 生成代码制品
  3. 03拦截 ORM 操作与 SQL
  4. 04添加实体变更生命周期钩子
  5. 05扩展客户端方法与查询参数
  6. 06计算查询结果虚拟字段

适用场景

  1. 01为模型字段添加自定义属性
  2. 02从 schema 生成报告或文件
  3. 03在写入数据库前转换 SQL 或数据
  4. 04为实体变更添加审计逻辑

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zenstackhq/skills --skill zenstack-plugin-dev

安装后在 Claude Code 中输入/zenstack-plugin-dev确认已加载。

详细介绍

ZenStack V3 — 插件开发

插件目前是预览版功能,未来版本中可能会发生变化。

ZenStack 插件可以单独或组合扩展工具包的三个层面:

  1. 模式(ZModel) — 为模式语言添加自定义属性(例如 @password)和函数。
  2. CLI(代码生成) — 参与 zen generate,根据模式生成产物。
  3. 运行时(ORM) — 安装到客户端,以拦截查询、挂接 CRUD 生命周期事件、转换 SQL、添加自定义方法或计算字段。

ZenStack 的 plugin 取代了 Prisma 的 generator。要了解如何在模式中声明和使用插件,请参阅 zenstack-schema-modeling;本指南介绍如何编写插件。

软件包:@zenstackhq/orm(运行时插件类型)、@zenstackhq/sdk(CLI 插件类型 + ZModel AST)。将 SDK 安装为开发依赖:npm install -D @zenstackhq/sdk。

在 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 软件包。CLI 插件必须是 ESM;TypeScript provider 通过 jiti 加载。内置的 @core/typescript 插件会自动运行;@core/prisma 会生成 Prisma 模式。

运行时插件

运行时插件是一个实现了 @zenstackhq/orm 中 RuntimePlugin 的对象。可以通过三种方式定义; definePlugin 能提供最佳的类型推断:

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… */
}

在客户端上安装(不可变 — 返回一个新客户端,原客户端保持不变);使用 $unuse(id) / $unuseAll() 移除。也可以在 ZenStackClient 构造函数中传入 plugins: [...]。

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] });

每个插件都需要一个唯一的 id。可以组合使用以下任意钩子。

onQuery — 拦截 ORM 操作

仅在调用 ORM API(findMany、create 等)时触发,不会在查询构建器上触发。可以通过 proceed 包装操作、修改参数、提前返回或后处理结果。

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 — 拦截/转换 SQL

这是最低层级的钩子:每次数据库访问(包括 ORM 和查询构建器)都会经由 Kysely,因此你可以在 调用 proceed 前检查或重写 SQL AST(OperationNode)。适用于软删除、注入过滤条件和列转换。

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 — 创建·更新·删除前后

围绕实体变更触发的生命周期钩子,并提供用于延迟加载受影响行的加载器。

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。注意: 数据库级联触发的变更不会被捕获,且在批量变更时,加载器可能开销较大。

扩展客户端(client)

在 client 下添加自定义方法/属性。名称必须以 $ 开头,以免与模型访问器冲突;建议归入同一个 $namespace 对象。

const cacheDb = db.$use(definePlugin({
    id: 'cache',
    client: {
        $cache: {
            get stats() { return { hits: 0, misses: 0 }; },
            async invalidate() { /* … */ },
        },
    },
}));
await cacheDb.$cache.invalidate();
自定义查询参数(queryArgs)

在 ORM 调用中接受额外选项,并使用 Zod 模式进行验证。可针对 $all、$create/$read/$update/$delete 组,或特定操作。

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 } });
计算结果字段(result,v3.5.0+)

添加虚拟字段,这些字段会在查询后根据声明的 needs 字段计算。

definePlugin({
    id: 'full-name',
    result: {
        user: {
            fullName: {
                needs: { firstName: true, lastName: true },
                compute: (user) => `${user.firstName} ${user.lastName}`,
            },
        },
    },
});

CLI 插件(代码生成)

CLI 插件是一个 ESM 模块,其默认导出为带有 generate 回调的 CliPlugin。 该插件在 zen generate 期间运行,并接收解析后的 ZModel AST。

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 代码块中的选项。

模式插件(自定义属性和函数)

在插件提供的 .zmodel 文件中声明新的 ZModel 属性/函数,然后在应用模式中使用。 @@@targetField 用于限制属性的适用位置。

// my-plugin/plugin.zmodel
attribute @password(hasher: Any) @@@targetField([StringField])
function bcryptHasher(rounds: Int): Any {}
// app schema
model User {
    id       Int    @id @default(autoincrement())
    password String @password(hasher: bcryptHasher(10))
}

端到端模式:密码哈希

文档中的示例将三个层级组合成一个插件包:

  1. Schema — plugin.zmodel 定义 @password + bcryptHasher();应用 Schema 将 @password(hasher: bcryptHasher(10)) 应用到某个字段。
  2. CLI — CliPlugin.generate 遍历 AST,并在设置 report 选项时生成“密码字段”报告。
  3. 运行时 — 一个带有 onKyselyQuery 的 RuntimePlugin 使用 Kysely OperationNodeTransformer 检测 @password 字段(通过 schema.models[...].fields[...].attributes),并在 INSERT/UPDATE 时、写入数据库之前对值进行哈希处理。

使用 db.$use(new PasswordHasherPlugin())(或 plugins 构造函数选项)进行安装,然后运行 npm install -D @zenstackhq/sdk 和 zen generate,执行 CLI 部分。

queryArgs Schema 请使用 Zod v4,以匹配 ZenStack 捆绑的版本。

参考文档

此主题的完整 ZenStack 文档已打包在 references/ 下:

评论

支持 Markdown