mcpField
Z
zenstack-querying

zenstack-querying

github.com/zenstackhqデータ・データベース3452 回インストール5 日前に更新
リポジトリ

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. 02ORMクエリへの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)を提供します。スキーマ構文については 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) }),
});

型はスキーマから完全に推論されるため、ほとんどのコードで個別にモデルをインポートする必要はありません。明示的な型が必要な場合は、クライアントには 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 } });

コールバックの第 2 引数は 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
}

フィールド間のルールには、ヘルパー関数 now()、length()、startsWith/endsWith/contains、isEmail/isUrl/isPhone/isDateTime、regex()、has/hasSome/hasEvery/isEmpty とともに、モデルレベルの @@validate(condition, message?, path?) を使用します。

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 を参照してください。検証ルール(または procedure 宣言を含む任意の ZModel)を編集した後は、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 呼び出しの結果は遅延プロミスであり、(直接、または $transaction 内で)await されるまで実行されません。

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();

ORM の where に Kysely 式を注入するには $expr を使用し、両者を組み合わせます。

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 バリアントはプレーンな文字列を受け取るため、インジェクションの危険があります。

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/に含まれています。

コメント