zenstack-plugin-dev について
zenstack-plugin-devは、ZModel、CLIコード生成、ORMランタイムを拡張するZenStack V3プラグイン開発スキルです。カスタム属性やクエリフックを作る開発者向けで、プラグイン機能は現在プレビュー版です。
主な機能
- 01ZModelの属性と関数を拡張
- 02CLIによる成果物の生成
- 03ORM操作とSQLのインターセプト
- 04エンティティ変更のライフサイクルフック
- 05クライアントメソッドとクエリ引数の追加
- 06仮想結果フィールドの計算
活用シーン
- 01モデルフィールドに独自属性を追加
- 02スキーマからレポートやファイルを生成
- 03データベース書き込み前にSQLやデータを変換
- 04エンティティ変更の監査処理を追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zenstackhq/ skills --skill zenstack-plugin-devインストール後、Claude Code で/zenstack-plugin-devを入力して読み込まれたか確認します。
詳細
ZenStack V3 — プラグイン開発
プラグインはプレビュー機能であり、今後のリリースで変更される可能性があります。
ZenStack プラグインは、3 つのレベルでツールキットを拡張できます。それぞれ単独で、または組み合わせて利用できます。
- スキーマ(ZModel) — カスタム属性(例:
@password)や関数をスキーマ言語に追加します。 - CLI(コード生成) — スキーマから成果物を生成するために、
zen generateに参加します。 - ランタイム(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 のプロバイダーは jiti を介して読み込まれます。組み込みの @core/typescript プラグインは自動的に実行され、@core/prisma は Prisma スキーマを出力します。
ランタイムプラグイン
ランタイムプラグインは、@zenstackhq/orm の RuntimePlugin を実装するオブジェクトです。定義方法は 3 通りあります。最も優れた型推論が得られるのは 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 をインターセプト/変換する
最も低レベルのフックです。すべての DB アクセス(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 を設定します。注:DB のカスケードによってトリガーされた変更は捕捉されず、一括変更ではローダーの処理コストが高くなることがあります。
クライアントの拡張(client)
カスタムメソッド/プロパティを client 配下に追加します。モデルアクセサーと名前が競合しないよう、名前は $ をプレフィックスにする必要があります。1 つの $namespace オブジェクトにまとめることを推奨します。
const cacheDb = db.$use(definePlugin({
id: 'cache',
client: {
$cache: {
get stats() { return { hits: 0, misses: 0 }; },
async invalidate() { /* … */ },
},
},
}));
await cacheDb.$cache.invalidate();
カスタムクエリー引数(queryArgs)
Zod スキーマで検証される追加オプションを ORM 呼び出しで受け付けます。$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 プラグインは、generate コールバックを持つ CliPlugin をデフォルトエクスポートする ESM モジュールです。パース済みの ZModel AST を受け取り、zen generate の実行中に動作します。
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))
}
エンドツーエンドのパターン:パスワードハッシャー
ドキュメントの具体例では、3つのレベルすべてを1つのプラグインパッケージにまとめています。
- スキーマ —
plugin.zmodelで@passwordとbcryptHasher()を定義し、アプリのスキーマでフィールドに@password(hasher: bcryptHasher(10))を適用します。 - CLI —
CliPlugin.generateが AST を走査し、reportオプションが設定されている場合に「パスワードフィールド」のレポートを出力します。 - ランタイム —
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スキーマには、ZenStack にバンドルされているバージョンと一致する Zod v4 を使用してください。
リファレンスドキュメント
このトピックに関する ZenStack のドキュメント一式は references/ に含まれています。
- plugin-dev.md — エンドツーエンドのプラグイン開発ガイド
- runtime-plugins-overview.md — ランタイムプラグインモデル
- extending-orm-client.md — クライアントメソッド、クエリ引数、計算フィールド
- entity-mutation-hooks.md — ミューテーション前後のフック
- kysely-query-hooks.md —
onKyselyQueryによる SQL 変換 - query-api-hooks.md —
onQueryによる ORM のインターセプト - zmodel-plugin.md — ZModel でのプラグイン宣言
- zmodel-plugin-reference.md —
pluginブロックのリファレンス