mcpField
Z
zenstack-crud-server

zenstack-crud-server

github.com/zenstackhq開発3456 回インストール2 日前に更新
リポジトリ

zenstack-crud-server について

zenstack-crud-serverは、データモデルからCRUDサービスを構築するZenStack V3のスキルです。RPC・RESTハンドラー、ポリシー適用クライアント、複数のWebフレームワーク向けアダプターを提供します。

主な機能

  1. 01RPC・REST APIの生成
  2. 02リクエストごとのポリシー適用ORMクライアント
  3. 03サーバーフレームワークへの統合
  4. 04公開するモデルや操作の制限
  5. 05OpenAPI仕様の生成
  6. 06非JSONデータ型のシリアライズ

活用シーン

  1. 01データモデルにCRUDエンドポイントを追加
  2. 02リクエストごとにユーザーポリシーを適用
  3. 03ExpressやNext.jsにAPIを統合
  4. 04OpenAPI仕様を公開

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/zenstackhq/skills --skill zenstack-crud-server

インストール後、Claude Code で/zenstack-crud-serverを入力して読み込まれたか確認します。

詳細

ZenStack V3 — 自動 CRUD サービス(サービスとしてのクエリ)

ZenStack は、手書きのコントローラーを作成せずに、データモデルをセキュアな CRUD Web API に変換します。次の 2 つの要素を組み合わせます。

  • API ハンドラー — フレームワークに依存せず、API のスタイル(RPC または RESTful)を定義し、HTTP リクエストを ORM クエリに変換します。@zenstackhq/server/api から利用できます。
  • サーバーアダプター — フレームワーク固有の連携機能です。ハンドラーを Express/Next.js などに登録し、リクエストごとのポリシー適用済み ORM クライアントを提供します。@zenstackhq/server/<framework> から利用できます。
HTTP request → server adapter → API handler (RPC | REST) → ZenStack ORM (policies applied) → DB

これは他のスキルを基盤としています。zenstack-schema-modeling でスキーマを定義し、zenstack-access-control で保護し、zenstack-querying でベースクライアントを作成します。サーバーパッケージをインストールします:npm install @zenstackhq/server。

API ハンドラー

RPC — ORM API を 1:1 で反映します。ルートは GET /post/findMany?q=<urlencoded-json> や POST /post/create(本文 { data: ... })のようになり、レスポンスは { data: ... } でラップされます。

import { RPCApiHandler } from '@zenstackhq/server/api';
import { schema } from '~/zenstack/schema';

const apiHandler = new RPCApiHandler({ schema });

RPC エンドポイント:すべての ORM 操作(findMany、findUnique、findFirst、count、aggregate、 groupBy、create、createMany、createManyAndReturn、upsert、update、updateMany、 updateManyAndReturn、delete、deleteMany)に加え、カスタムプロシージャ用の $procs/<name> と $transaction/sequential。ステータスコード:201 は作成、200 はその他の成功、400 は不正な形式、403 はポリシー違反、404 は未検出、422 は検証エラー、500 は予期しないエラーです。

RESTful — JSON:API v1.1 に準拠しています。ルートは GET /post、GET /post/:id、POST /post、PUT|PATCH /post/:id、DELETE /post/:id と、リレーションシップのルートなどです。

import { RestApiHandler } from '@zenstackhq/server/api'; // note: RestApiHandler, not RESTful

const apiHandler = new RestApiHandler({
    schema,
    endpoint: 'http://localhost:3000/api', // required — used to build resource links
});

RestApiHandler のオプション:endpoint(必須)、pageSize(デフォルトは 100、Infinity でページングを無効化)、modelNameMapping({ User: 'users' })、externalIdMapping({ Tag: 'name' })、nestedRoutes(ブール値)。REST クエリパラメーター:filter[field]=、filter[field$op]=($lt、$gt、$contains、$startsWith など)、sort=field,-other、page[offset]=/page[limit]=、include=rel,rel.nested、fields[type]=a,b。

どちらのハンドラーも、API が公開する内容を絞り込み/除外するための queryOptions を受け取ります。

new RPCApiHandler({
    schema,
    queryOptions: {
        slicing: {
            includedModels: ['User', 'Post'],
            models: { post: { excludedOperations: ['delete'] } },
        },
        omit: { user: { password: true } },
    },
});

サーバーアダプター — アクセス制御は getClient で設定

すべてのアダプターは apiHandler と getClient(request) => ClientContract コールバックを受け取ります。リクエストごとに分離されるよう、$setAuth を介して現在のユーザーに紐づいたポリシー適用済みクライアントを返します(zenstack-access-control を参照)。

getClient: (req) => authDb.$setAuth(getSessionUser(req)),

authDb は db.$use(new PolicyPlugin()) です。匿名ユーザーには authDb.$setAuth(undefined) を返し、ポリシーを回避する場合(公開 API では通常必要ありません)は生の db を返します。

Express
import { ZenStackMiddleware } from '@zenstackhq/server/express';

app.use(express.json());
app.use(
    '/api/model',
    ZenStackMiddleware({
        apiHandler,
        getClient: (req) => authDb.$setAuth(getSessionUser(req)),
    }),
);

(任意の sendResponse: false は res.locals に書き込み、代わりに next() を呼び出します。)

Fastify
import { ZenStackFastifyPlugin } from '@zenstackhq/server/fastify';

server.register(ZenStackFastifyPlugin, {
    prefix: '/api/model', // required
    apiHandler,
    getClient: (req) => authDb.$setAuth(getSessionUser(req)),
});
Next.js — App Router
// src/app/api/model/[...path]/route.ts
import { NextRequestHandler } from '@zenstackhq/server/next';

const handler = NextRequestHandler({
    apiHandler,
    getClient: (req) => authDb.$setAuth(getSessionUser(req)),
    useAppDir: true,
});
export {
    handler as GET,
    handler as POST,
    handler as PUT,
    handler as PATCH,
    handler as DELETE,
};
Next.js — Pages Router
// src/pages/api/model/[...path].ts
import { NextRequestHandler } from '@zenstackhq/server/next';

export default NextRequestHandler({
    apiHandler,
    getClient: (req, res) => authDb.$setAuth(getSessionUser(req, res)),
});
Nuxt
// server/api/model/[...].ts
import { createEventHandler } from '@zenstackhq/server/nuxt';

export default createEventHandler({
    apiHandler,
    getClient: (event) => authDb.$setAuth(getSessionUser(event)),
});
SvelteKit(API ルート — 推奨。ワイルドカードパラメーター名は path にする必要があります)
// src/routes/api/model/[...path]/+server.ts
import { SvelteKitRouteHandler } from '@zenstackhq/server/sveltekit';

const handler = SvelteKitRouteHandler({
    apiHandler,
    getClient: (event) => authDb.$setAuth(getSessionUser(event)),
});
export const GET = handler,
    POST = handler,
    PUT = handler,
    PATCH = handler,
    DELETE = handler;

(prefix オプションを使う hooks.server.ts のレガシーな SvelteKitHandler は非推奨です。)

Hono
import { createHonoHandler } from '@zenstackhq/server/hono';

app.use(
    '/api/model/*',
    createHonoHandler({
        apiHandler,
        getClient: (ctx) => authDb.$setAuth(getSessionUser(ctx)),
    }),
);
Elysia
import { createElysiaHandler } from '@zenstackhq/server/elysia';

app.group('/crud', (app) =>
    app.use(
        createElysiaHandler({
            apiHandler,
            basePath: '/api/model',
            getClient: (ctx) => authDb.$setAuth(getSessionUser(ctx)),
        }),
    ),
);
TanStack Start
// app/routes/api/$.ts
import { TanStackStartHandler } from '@zenstackhq/server/tanstack-start';

const handler = TanStackStartHandler({
    apiHandler,
    getClient: (req) => authDb.$setAuth(getSessionUser(req)),
});
export const Route = createFileRoute('/api/$')({
    server: {
        handlers: {
            GET: handler,
            POST: handler,
            PUT: handler,
            PATCH: handler,
            DELETE: handler,
        },
    },
});

データ型のシリアル化

ハンドラーは superjson を使用するため、JSON では扱えない型も通信時に保持されます:DateTime → ISO 文字列、Bytes → base64、BigInt/Decimal → 文字列。生成されたクライアント SDK はこれを自動的に処理します。リクエストを手動で作成する場合は、superjson の meta を serialization キーの下に含めます ({ "data": ..., "meta": { "serialization": <meta> } })。レスポンスにも同じ情報が含まれます。

API の利用

Fetch クライアント(v3.7.0 以降、RPC API のみ)
import { createClient } from '@zenstackhq/fetch-client';
import { schema } from '~/zenstack/schema';

const client = createClient(schema, {
    endpoint: 'https://example.com/api/model',
});
const users = await client.user.findMany({ include: { posts: true } });
await client.post.create({ data: { title: 'Hello' } });

カスタム fetch を使って認証を追加します。

import type { FetchFn } from '@zenstackhq/fetch-client';
const fetchFn: FetchFn = (url, init) =>
    fetch(url, {
        ...init,
        headers: { ...init?.headers, authorization: `Bearer ${getToken()}` },
    });
createClient(schema, { endpoint, fetch: fetchFn });

カスタムプロシージャ:client.$procs.getStats.query() / client.$procs.send.mutate({ args })。 シーケンシャル tx:client.$transaction([{ model, op, args }, ...])。

TanStack Query フック(RPC API のみ。React 18+/Vue 3+/Svelte 5.25+)

@zenstackhq/tanstack-query と、対応する @tanstack/*-query をインストールします。ルート付近で設定を指定します(React の例を示します。Vue では provideQuerySettingsContext、Svelte では setQuerySettingsContext を使用します)。

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QuerySettingsProvider } from '@zenstackhq/tanstack-query/react';

<QueryClientProvider client={new QueryClient()}>
    <QuerySettingsProvider value={{ endpoint: '/api/model' }}>
        {children}
    </QuerySettingsProvider>
</QueryClientProvider>;

useClientQueries(schema) を使って、ORM クライアントに対応するフックを利用できます。

import { useClientQueries } from '@zenstackhq/tanstack-query/react';

const client = useClientQueries(schema);
const { data } = client.user.useFindMany({ include: { posts: true } });
const create = client.post.useCreate();
create.mutate({ data: { title: 'New post' } });
  • 自動無効化:ミューテーションによって、影響を受けるクエリが自動的に無効化されます(ネストされた読み取り/書き込みとカスケードにも対応します)。ミューテーションごとに { invalidateQueries: false } を指定して無効化できます。
  • 楽観的更新:client.post.useCreate({ optimisticUpdate: true }) を使用します。optimisticUpdateProvider でカスタマイズできます。
  • その他:useInfiniteFindMany、$procs.<name>.useQuery()/useMutation()、および JSON の null 値に使用する DbNull/JsonNull/AnyNull エクスポート。

コミュニティによる Pinia Colada 統合として、zenstack-pinia-colada(Vue 3)が用意されています。

OpenAPI 仕様

どちらのハンドラーでも、generateSpec() を使って OpenAPI 仕様を生成できます(RPC は v3.6.0+、REST は v3.5.0+)。

import type { OpenApiSpecOptions } from '@zenstackhq/server/api';

app.get('/api/openapi.json', async (_req, res) => {
    const spec = await apiHandler.generateSpec({
        title: 'My Blog API',
        version: '2.0.0',
        respectAccessPolicies: true, // emit 403 responses for policy-protected models
    });
    res.json(spec);
});

オプション:title(デフォルトは 'ZenStack Generated API')、version(デフォルトは '1.0.0')、description、summary、respectAccessPolicies(デフォルトは false)。ハンドラーの queryOptions.slicing/omit も、仕様に含まれる内容に影響します。

リファレンスドキュメント

このトピックに関する ZenStack のドキュメント一式は、references/ に同梱されています。

コメント