mcpField
Z
zenstack-querying

zenstack-querying

github.com/zenstackhq数据与数据库3439 次安装2 天前更新
代码仓库

关于 zenstack-querying

zenstack-querying 是 ZenStack V3 数据工具包的查询技能,面向使用 TypeScript 操作数据库的开发者。它涵盖 Prisma 兼容 ORM、Kysely 查询构建器及验证、事务等能力。

主要功能

  1. 01Prisma 兼容 ORM 查询
  2. 02Kysely 查询构建器
  3. 03关联查询与嵌套写入
  4. 04数据库端计算字段
  5. 05输入验证与事务
  6. 06原始 SQL 与自定义过程

适用场景

  1. 01按条件筛选、排序和聚合数据
  2. 02在 ORM 查询中混合 SQL 表达式
  3. 03执行多步骤事务与关联写入

安装

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

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

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

详细介绍

ZenStack V3 — 查询

ZenStack 客户端提供兼容 Prisma 的 ORM API,用于日常查询;对于 ORM API 无法表达的 SQL 形状查询,则提供 Kysely 查询构建器($qb)。Schema 语法请参阅 zenstack-schema-modeling;访问控制请参阅 zenstack-access-control。

创建客户端

从 @zenstackhq/orm 导入 ZenStackClient,并传入生成的 schema 和 Kysely dialect。 请自行安装匹配的驱动程序(参见 zenstack-project-setup)。

SQLite(better-sqlite3):

import { ZenStackClient } from '@zenstackhq/orm';
import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite';
import SQLite from 'better-sqlite3';
import { schema } from './zenstack/schema';

export const db = new ZenStackClient(schema, {
    dialect: new SqliteDialect({ database: new SQLite('./dev.db') }),
});

PostgreSQL(pg):

import { ZenStackClient } from '@zenstackhq/orm';
import { PostgresDialect } from '@zenstackhq/orm/dialects/postgres';
import { schema } from './zenstack/schema';
import { Pool } from 'pg';

export const db = new ZenStackClient(schema, {
    dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) }),
});

MySQL(mysql2):

import { MysqlDialect } from '@zenstackhq/orm/dialects/mysql';
import { createPool } from 'mysql2';

export const db = new ZenStackClient(schema, {
    dialect: new MysqlDialect({ pool: createPool(process.env.DATABASE_URL) }),
});

类型会根据 schema 完整推断,因此大多数代码都无需单独导入模型。需要显式类型时:客户端可使用 ClientContract<SchemaType>;也可导入生成的 models / input 类型,或使用 @zenstackhq/orm 中的 ModelResult 泛型,根据给定的 select / include 计算行类型。

如需强制执行访问策略,请用策略插件包装此客户端 — 参见 zenstack-access-control。

ORM API

模型访问器位于客户端上(db.user、db.post……)。此 API 与 Prisma Client 一致。

读取:findMany、findUnique、findUniqueOrThrow、findFirst、findFirstOrThrow、 exists(v3.2.0+,比 findFirst / count 开销更低)、count、aggregate、groupBy。

const users = await db.user.findMany({
    where: { age: { gt: 18 }, email: { endsWith: '@acme.com' } },
    orderBy: { createdAt: 'desc' },
    skip: 0,
    take: 20,
});
const user = await db.user.findUnique({ where: { id: 1 } });
const n = await db.user.count({ where: { age: { gte: 18 } } });

筛选运算符:equals、not、in、notIn、contains、startsWith、endsWith、lt、 lte、gt、gte、between;逻辑运算符 AND / OR / NOT;列表运算符 has / hasEvery / hasSome / isEmpty (Postgres)。对于一对多关系,使用 some / every / none 筛选;对于一对一关系,直接筛选字段。

聚合 / groupBy 支持 _count、_sum、_avg、_min、_max;groupBy 使用 having 筛选 聚合结果:

await db.post.groupBy({ by: 'authorId', _count: true, having: { viewCount: { _sum: { gt: 100 } } } });

创建:create(支持嵌套写入,并在事务中运行)、createMany(不支持嵌套关系;返回计数)、createManyAndReturn。

await db.user.create({
    data: { email: '[email protected]', posts: { create: [{ title: 'Hello' }] } },
});

更新:update、updateMany / updateManyAndReturn(仅支持标量字段)、upsert。列表 字段使用 push / set:

await db.post.update({ where: { id: 1 }, data: { topics: { push: 'webdev' } } });
await db.post.upsert({
    where: { id: 1 },
    create: { id: 1, title: 'New' },
    update: { title: 'Updated' },
});

删除:delete、deleteMany、deleteManyAndReturn。

关系查询 — select / include / omit

  • select:选择特定字段;对于关系,传入嵌套对象。不能与 include / omit 同时使用。
  • include:提取关系(所有字段使用 true,或传入对象以筛选关系 / 使用 where 过滤关系)。 不能与 select 同时使用。
  • omit:排除特定标量字段。不能与 select 同时使用。
await db.user.findMany({
    include: { posts: { where: { published: true }, select: { id: true, title: true } } },
});

create / update / upsert 中的嵌套写入可以对相关记录执行 create、connect、disconnect、更新和删除(支持深层嵌套),所有操作都在一个事务中完成。

计算字段

在 ZModel 中声明为 @computed 的字段(参见 zenstack-schema-modeling)会在数据库端进行计算。在客户端选项 computedFields 中以 Kysely 表达式提供其实现;之后,它的行为就像常规字段一样 — 查询会返回该字段,并可在 select、where、orderBy 和 aggregate 中使用。

const db = new ZenStackClient(schema, {
    dialect,
    computedFields: {
        User: {
            // SQL: (SELECT COUNT(*) FROM "Post" WHERE "Post"."authorId" = "User"."id")
            postCount: (eb) =>
                eb.selectFrom('Post')
                    .whereRef('Post.authorId', '=', 'id')
                    .select(({ fn }) => fn.countAll<number>().as('count')), // selection must be named
        },
    },
});

await db.user.findFirst();                                   // postCount included
await db.user.findFirst({ select: { email: true, postCount: true } });
await db.user.findMany({ where: { postCount: { gt: 1 } }, orderBy: { postCount: 'desc' } });
await db.user.aggregate({ _avg: { postCount: true } });

回调的第二个参数是一个包含 modelAlias 的 context;遇到冲突时,可使用 sql.ref(\${modelAlias}.id`)(从 @zenstackhq/orm/helpers导入sql`)限定所属模型的列。

多态模型

对于使用 @@delegate 继承的模型(参见 zenstack-schema-modeling),可通过常规模型访问器进行查询:

  • 创建具体模型(db.video.create(...))— 基础行会自动创建。不能直接创建基础模型。
  • 查询基础模型(db.content.findMany()),每个结果都会包含具体模型的字段(除非使用 select 缩小字段范围)。查询具体模型会返回基础模型和具体模型的字段。
  • 删除任一侧时,对应的另一行也会一并删除。
const user = await db.user.create({ data: { email: '[email protected]' } });
await db.post.create({ data: { name: 'Post1', content: 'Hi', ownerId: user.id } });
await db.video.create({ data: { name: 'Video1', url: 'http://v/1', ownerId: user.id } });

const content = await db.content.findFirstOrThrow();   // includes concrete fields
await db.user.findFirstOrThrow({ include: { contents: true } });

判别值的类型是字符串字面量(或枚举成员),因此检查该值会将结果类型收窄为匹配的具体模型:

const c = await db.content.findUniqueOrThrow({ where: { id: 1 } });
if (c.type === 'video') console.log(c.url);    // narrowed to Video
else if (c.type === 'image') console.log(c.data);

强类型 JSON

使用自定义 type + @json 声明的字段(参见 zenstack-schema-modeling)会以强类型形式返回(类型派生自 ZModel 类型),并且创建/更新时会根据该类型验证输入。验证较为宽松——允许包含类型中未定义的额外字段。

// query results are typed: user.profile.age / user.profile.gender
const user = await db.user.create({
    data: { email: '[email protected]', profile: { gender: 'male', age: 20 } },
});

await db.user.update({
    where: { id: user.id },
    data: { profile: { ...user.profile, tag: 'vip' } }, // extra `tag` is allowed
});

数据验证

在 ZModel 中声明验证规则,并由 ORM 对 create/update 输入执行验证(不会对查询构建器 / 原始 SQL 执行验证)。验证失败时会抛出原因代码为 INVALID_INPUT 的 ORMError。每个属性都可以附加一个可选的尾随 message。

字段属性

  • 字符串长度 / 列表:@length(min?, max?)
  • 字符串内容:@startsWith、@endsWith、@contains、@email、@url、@phone(E.164)、@datetime(ISO 8601)、@regex(pattern)
  • 字符串转换(保存前应用):@lower、@upper、@trim
  • 数字:@gt、@gte、@lt、@lte
model User {
    email    String  @email("Invalid email")
    password String  @length(8, 128)
    age      Int     @gte(0) @lte(150)
    website  String? @url
}

模型级 @@validate(condition, message?, path?) 用于跨字段规则,并支持以下辅助函数:now()、length()、startsWith/endsWith/contains、isEmail/isUrl/isPhone/isDateTime、regex()、has/hasSome/hasEvery/isEmpty。

model Event {
    startDate DateTime
    endDate   DateTime
    tags      String[]
    @@validate(endDate > startDate, "End date must be after start date")
    @@validate(!isEmpty(tags), "At least one tag is required")
}

验证会在访问策略之前运行,并且在访问数据库之前完成。有关访问控制,请参阅 zenstack-access-control。编辑验证规则(或任何 ZModel 内容,包括 procedure 声明)后,运行 zen generate 以重新生成客户端(该命令也会验证架构);如果只想验证而不重新生成,请运行 zen check。

事务 — $transaction

顺序执行 — 操作数组,按顺序执行,不支持跨操作访问:

const [a, b] = await db.$transaction([
    db.user.create({ data: { name: 'Alice' } }),
    db.user.create({ data: { name: 'Bob' } }),
]);

交互式 — 回调接收事务客户端;操作结果可供后续步骤使用。请保持回调简短。

const post = await db.$transaction(async (tx) => {
    const user = await tx.user.create({ data: { name: 'Alice' } });
    return tx.post.create({ data: { title: 'Hi', authorId: user.id } });
});

ORM 调用结果是惰性 Promise——只有在被等待时(直接等待或在 $transaction 内等待)才会执行。

Kysely 查询构建器 — $qb 和 $expr

当 ORM API 无法满足需求时,可使用完整的 Kysely API(类型基于架构)。有关构建器 API,请参阅 https://kysely.dev。

const rows = await db.$qb
    .selectFrom('User')
    .select(['id', 'name'])
    .where('age', '>', 18)
    .execute();

使用 $expr 将 Kysely 表达式注入 ORM 的 where 中,从而混合使用两者:

await db.post.findMany({
    where: {
        published: true,
        $expr: (eb) => eb('viewCount', '>', 100),
    },
});

查询构建器会绕过访问策略和 ORM 级验证——安装了策略插件时,请牢记这一点。

自定义过程(v3.2.0+)

在 ZModel 中声明带类型的操作,在客户端构造时实现,并通过 $procs 调用。

procedure getUserFeeds(userId: Int, limit: Int?) : Post[]
mutation procedure signUp(email: String) : User
const db = new ZenStackClient(schema, {
    dialect,
    procedures: {
        getUserFeeds: ({ client, args }) =>
            client.post.findMany({ where: { authorId: args.userId }, take: args.limit }),
        signUp: ({ client, args }) => client.user.create({ data: { email: args.email } }),
    },
});

const user = await db.$procs.signUp({ args: { email: '[email protected]' } });
const feeds = await db.$procs.getUserFeeds({ args: { userId: user.id, limit: 20 } });

回调运行前,会根据 ZModel 类型验证参数;不会验证返回值——请自行确保其符合要求。发生错误时,抛出 ORMError。

原始 SQL

$queryRaw / $executeRaw 使用带标签的模板(参数化且安全);*Unsafe 变体接收普通字符串,存在 SQL 注入风险。

const users = await db.$queryRaw<{ id: number; email: string }[]>`
    SELECT id, email FROM "User" WHERE role = ${role}`;
const affected = await db.$executeRaw`UPDATE "User" SET name = ${name} WHERE id = ${id}`;

原始 SQL 会绕过访问控制;安装策略插件后,必须设置 dangerouslyAllowRawSql。

日志记录

向 ZenStackClient 传入 log 选项——可以是级别数组,也可以是函数。该选项会转发给底层 Kysely 实例。

const db = new ZenStackClient(schema, {
    dialect,
    log: ['query', 'error'],
    // or a function:
    // log: (event) => console.log(`[${event.level}] ${event.queryDurationMillis}ms`),
});

错误处理

所有 ORM 错误都是 ORMError。检查 error.reason(CONFIG_ERROR、INVALID_INPUT、NOT_FOUND、REJECTED_BY_POLICY、DB_QUERY_ERROR、NOT_SUPPORTED、INTERNAL_ERROR);其他字段包括 model、dbErrorCode、dbErrorMessage、rejectedByPolicyReason,以及(针对 DB_QUERY_ERROR)sql/sqlParams。

import { ORMError } from '@zenstackhq/orm';

try {
    await userDb.post.create({ data: { title: '' } });
} catch (e) {
    if (e instanceof ORMError && e.reason === 'REJECTED_BY_POLICY') {
        // handle access-denied
    }
}

参考文档

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

评论

支持 Markdown