mcpField
Z
zenstack-crud-server

zenstack-crud-server

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

关于 zenstack-crud-server

zenstack-crud-server 是 ZenStack V3 的 CRUD 服务开发技能,可根据数据模型生成受策略保护的 RPC 或 REST API,并适配多种 Web 框架。适合构建数据服务的开发者。

主要功能

  1. 01生成 RPC 与 REST API
  2. 02按请求绑定策略化 ORM 客户端
  3. 03适配多种服务端框架
  4. 04限制 API 暴露的模型与操作
  5. 05生成 OpenAPI 规范
  6. 06序列化非 JSON 数据类型

适用场景

  1. 01为模型快速提供 CRUD 接口
  2. 02按当前用户策略隔离请求数据
  3. 03在 Express 或 Next.js 集成 API
  4. 04生成可供工具使用的 OpenAPI 文档

安装

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

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

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

详细介绍

ZenStack V3 — 自动 CRUD 服务(查询即服务)

ZenStack 可将数据模型转换为安全的 CRUD Web API,无需手写控制器。它由两个部分组成:

  • 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 一一对应。路由示例: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。

两个处理器还都接受 queryOptions,用于切片/省略 API 暴露的内容:

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);也可以返回原始 db 以绕过策略(对于公共 API,这通常不是你想要的行为)。

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;

(已弃用旧版 SvelteKitHandler,它位于 hooks.server.ts 中并带有 prefix 选项。)

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 会自动处理这些类型。若要手动构造请求,请在 serialization 键下包含 superjson meta({ "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 })。 顺序事务:client.$transaction([{ model, op, args }, ...])。

TanStack Query hooks(仅限 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),hooks 与 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' } });
  • 自动失效:mutation 会自动使受影响的查询失效(支持嵌套读写和级联)。可通过 { invalidateQueries: false } 针对单个 mutation 关闭此功能。
  • 乐观更新:client.post.useCreate({ optimisticUpdate: true });可使用 optimisticUpdateProvider 自定义。
  • 其他功能:useInfiniteFindMany、$procs.<name>.useQuery()/useMutation()、$transaction.useSequential(),以及用于 JSON null 的 DbNull/JsonNull/AnyNull 导出项。

社区还提供了 zenstack-pinia-colada 集成(Vue 3)。

OpenAPI 规范

两个 handler 均可通过 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)。handler 上的 queryOptions.slicing/omit 也会影响规范中包含的内容。

参考文档

此主题的完整 ZenStack 文档已随附在 references/ 目录下:

评论

支持 Markdown