AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Zenstack Querying

skill-zenstackhq-skills-zenstack-querying · by zenstackhq

Query and mutate data with the ZenStack V3 client. Use when creating a ZenStackClient (SQLite/Postgres/MySQL dialect), using the Prisma-compatible ORM API (findMany/create/update/etc.), relation queries (select/include), computed fields, polymorphic models, strongly-typed JSON, data validation on writes (@email/@length/@@validate), transactions, the Kysely query-builder escape hatch ($qb / $expr)…

No reviews yet
0 installs
32 views
0.0% view→install

Install

$ agentstack add skill-zenstackhq-skills-zenstack-querying

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-zenstackhq-skills-zenstack-querying)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Zenstack Querying? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

ZenStack V3 — Querying

The ZenStack client exposes a Prisma-compatible ORM API for everyday queries plus a Kysely query builder ($qb) for anything SQL-shaped that the ORM API can't express. Schema syntax is in zenstack-schema-modeling; access control in zenstack-access-control.

Creating the client

Import ZenStackClient from @zenstackhq/orm, pass the generated schema plus a Kysely dialect. Install the matching driver yourself (see 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) }),
});

Types are fully inferred from the schema — no separate model imports needed for most code. When you need an explicit type: ClientContract for the client, or import generated models / input types, or use the ModelResult generic from @zenstackhq/orm to compute a row type from a given select/include.

To enforce access policies, wrap this client with the policy plugin — see zenstack-access-control.

ORM API

Model accessors live on the client (db.user, db.post, …). The API matches Prisma Client.

Read: findMany, findUnique, findUniqueOrThrow, findFirst, findFirstOrThrow, exists (v3.2.0+, cheaper than 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 } } });

Filter operators: equals, not, in, notIn, contains, startsWith, endsWith, lt, lte, gt, gte, between; logical AND/OR/NOT; list has/hasEvery/hasSome/isEmpty (Postgres). For to-many relations filter with some/every/none; for to-one, filter fields directly.

Aggregate / groupBy support _count, _sum, _avg, _min, _max; groupBy filters aggregates via having:

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

Create: create (supports nested writes, runs in a transaction), createMany (no nested relations; returns a count), createManyAndReturn.

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

Update: update, updateMany / updateManyAndReturn (scalar fields only), upsert. List fields use 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: delete, deleteMany, deleteManyAndReturn.

Relation queries — select / include / omit

  • select: pick specific fields; for a relation pass a nested object. Mutually exclusive with

include/omit.

  • include: pull in relations (true for all fields, or an object to filter/where the relation).

Mutually exclusive with select.

  • omit: drop specific scalar fields. Mutually exclusive with select.
await db.user.findMany({
    include: { posts: { where: { published: true }, select: { id: true, title: true } } },
});

Nested writes inside create/update/upsert can create, connect, disconnect, update, and delete related records (deeply), all within a transaction.

Computed fields

A field declared @computed in ZModel (see zenstack-schema-modeling) is evaluated on the database side. Provide its implementation as a Kysely expression in the computedFields client option; thereafter it behaves like a regular field — returned by queries, and usable in select, where, orderBy, and 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().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 } });

The callback's second arg is a context with modelAlias — use sql.ref(\${modelAlias}.id\) (import sql from @zenstackhq/orm/helpers) to qualify the containing model's columns on conflicts.

Polymorphic models

For models using @@delegate inheritance (see zenstack-schema-modeling), query via the usual model accessors:

  • Create concrete models (db.video.create(...)) — the base row is created automatically. Base

models cannot be created directly.

  • Query a base model (db.content.findMany()) and each result includes the concrete model's

fields (unless you narrow with select). Querying a concrete model returns base + concrete fields.

  • Delete either side and the counterpart row is removed too.
const user = await db.user.create({ data: { email: 'u1@test.com' } });
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 } });

The discriminator value is typed as a string literal (or enum member), so checking it narrows the result type to the matching concrete model:

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

Strongly-typed JSON

A field typed with a custom type + @json (see zenstack-schema-modeling) is returned strongly typed (derived from the ZModel type), and inputs are validated against that type on create/update. Validation is loose — extra fields not in the type are allowed.

// query results are typed: user.profile.age / user.profile.gender
const user = await db.user.create({
    data: { email: 'u1@test.com', profile: { gender: 'male', age: 20 } },
});

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

Data validation

Validation rules are declared in ZModel and enforced by the ORM on create/update inputs (not on the query builder / raw SQL). Failures throw ORMError with reason INVALID_INPUT. Each attribute takes an optional trailing message.

Field attributes

  • String length / lists: @length(min?, max?)
  • String content: @startsWith, @endsWith, @contains, @email, @url, @phone (E.164),

@datetime (ISO 8601), @regex(pattern)

  • String transforms (applied before save): @lower, @upper, @trim
  • Numbers: @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
}

Model-level @@validate(condition, message?, path?) for cross-field rules, with helper functions: 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")
}

Validation runs before access policies and before the database is touched. For access control see zenstack-access-control. After editing validation rules (or any ZModel — including procedure declarations), run zen generate to regenerate the client (it also validates the schema); run zen check if you only want to validate without regenerating.

Transactions — $transaction

Sequential — array of operations, executed in order, no cross-access:

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

Interactive — callback receiving a transaction client; results feed later steps. Keep it short.

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 call results are lazy promises — they don't run until awaited (directly or inside > $transaction).

Kysely query builder — $qb and $expr

Use the full Kysely API (typed from the schema) when the ORM API isn't enough. See https://kysely.dev for the builder API.

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

Inject a Kysely expression into an ORM where with $expr to mix the two:

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

> The query builder bypasses access policies and ORM-level validation — keep that in mind when a > policy plugin is installed.

Custom procedures (v3.2.0+)

Declare typed operations in ZModel, implement them at client construction, call via $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: 'alice@example.com' } });
const feeds = await db.$procs.getUserFeeds({ args: { userId: user.id, limit: 20 } });

Args are validated against the ZModel types before the callback runs; return values are not — match them yourself. Throw ORMError for failures.

Raw SQL

$queryRaw / $executeRaw use tagged templates (parameterized, safe); the *Unsafe variants take plain strings and are injection-prone.

const users = await db.$queryRaw`
    SELECT id, email FROM "User" WHERE role = ${role}`;
const affected = await db.$executeRaw`UPDATE "User" SET name = ${name} WHERE id = ${id}`;

Raw SQL bypasses access control; with the policy plugin installed it requires dangerouslyAllowRawSql.

Logging

Pass a log option to ZenStackClient — an array of levels, or a function. It's forwarded to the underlying Kysely instance.

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

Error handling

All ORM errors are ORMError. Inspect error.reason (CONFIG_ERROR, INVALID_INPUT, NOT_FOUND, REJECTED_BY_POLICY, DB_QUERY_ERROR, NOT_SUPPORTED, INTERNAL_ERROR); other fields include model, dbErrorCode, dbErrorMessage, rejectedByPolicyReason, and (for 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
    }
}

Reference docs

Full ZenStack documentation for this topic is bundled under [references/](references/):

  • [client.md](references/client.md) — creating the ZenStackClient (SQLite/Postgres/MySQL dialects)
  • [orm-overview.md](references/orm-overview.md) — ORM overview
  • [api-overview.md](references/api-overview.md) — ORM API overview
  • [api-find.md](references/api-find.md) — find operations
  • [api-create.md](references/api-create.md) — create / nested writes
  • [api-update.md](references/api-update.md) — update / upsert / nested writes
  • [api-delete.md](references/api-delete.md) — delete operations
  • [api-filter.md](references/api-filter.md) — filter operators
  • [api-aggregate.md](references/api-aggregate.md) — aggregate
  • [api-count.md](references/api-count.md) — count
  • [api-group-by.md](references/api-group-by.md) — groupBy
  • [api-transaction.md](references/api-transaction.md) — $transaction
  • [api-raw-sql.md](references/api-raw-sql.md) — raw SQL
  • [query-builder.md](references/query-builder.md) — the Kysely $qb escape hatch
  • [custom-procedures.md](references/custom-procedures.md) — custom procedures
  • [computed-fields.md](references/computed-fields.md) — computed fields at runtime
  • [polymorphism.md](references/polymorphism.md) — querying polymorphic models
  • [typed-json.md](references/typed-json.md) — strongly-typed JSON fields at runtime
  • [validation.md](references/validation.md) — data validation
  • [input-validation-reference.md](references/input-validation-reference.md) — validation attribute reference
  • [inferred-types.md](references/inferred-types.md) — inferred model/input types
  • [logging.md](references/logging.md) — client logging configuration
  • [errors.md](references/errors.md) — ORMError and error reasons

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.