npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zenstack-graphql

v0.2.0

Published

A standalone GraphQL adapter for ZenStack V3 with Hasura-like CRUD conventions.

Readme

zenstack-graphql

zenstack-graphql is a standalone GraphQL adapter for ZenStack-style model metadata. It generates a framework-agnostic GraphQLSchema with Hasura-like CRUD roots, model-driven filters and ordering, aggregates, nested relation inserts, core insert/update/delete mutations, ZenStack procedure roots, and optional custom root resolvers.

ZenStack is a schema-first TypeScript data platform and ORM that lets you model your data, access policies, and API-facing behavior in ZModel, then generate a typed runtime on top of your database. This package is the GraphQL layer that sits on top of that metadata and exposes it with Hasura-inspired conventions.

Requirements

  • Node.js >=18.17
  • graphql ^16.11.0 as a peer dependency
  • ZenStack V3 schema metadata and a request-scoped ZenStack client

Install

npm install zenstack-graphql graphql

Choose Your Surface

Use the lowest-level API that matches your app:

  • zenstack-graphql/core
    • For direct schema generation and custom GraphQL server wiring
  • zenstack-graphql/server
    • For the framework-agnostic transport handler
  • zenstack-graphql/hasura
    • For convenience helpers around x-hasura-role request extraction and schema slicing
  • zenstack-graphql
    • Convenience root export that re-exports the full public surface

Core Usage

import { createZenStackGraphQLSchema } from 'zenstack-graphql/core';

const schema = createZenStackGraphQLSchema({
    schema: {
        models: [
            {
                name: 'User',
                fields: [
                    { name: 'id', kind: 'scalar', type: 'Int', isId: true },
                    { name: 'name', kind: 'scalar', type: 'String' },
                ],
            },
        ],
    },
    async getClient(context) {
        return context.db;
    },
});

Public API

  • createZenStackGraphQLSchema({ schema, getClient, compatibility, naming, features, relay, slicing, scalars, scalarAliases, hooks, extensions })
  • createZenStackGraphQLSchemaFactory({ schema, getClient, getSlicing, getCacheKey, ... })
  • new GraphQLApiHandler({ schema, getSlicing, getCacheKey, ... })
  • createFetchGraphQLHandler(...)
  • normalizeSchema(schema)
  • normalizeError(error)

The generated schema uses Hasura-like defaults:

  • Query roots: users, users_by_pk, users_aggregate
  • Mutation roots: insert_users, insert_users_one, update_users, update_users_by_pk, delete_users, delete_users_by_pk
  • String filters include Hasura-style pattern operators like _like, _nlike, _ilike, and _nilike, plus extended prefix/suffix/contains variants
  • Provider-specific filters now include PostgreSQL scalar-list operators (has, hasEvery, hasSome, isEmpty) and ZenStack-style Json filters with JSON-path support
  • Comparable scalar filters include _between
  • Strongly typed JSON / typedef-backed fields can be filtered recursively, including list-object filters with some, every, and none
  • insert_* and insert_*_one support on_conflict
  • *_insert_input supports nested relation data inserts
  • *_set_input supports relation-aware updates for the nested mutation shapes supported by the underlying ZenStack ORM
  • To-many relation filters support the ORM-backed some, every, and none semantics via additive GraphQL fields like posts_some, posts_every, and posts_none
  • features.computedFields enables read-only @computed fields detected from ZenStack-generated metadata
  • slicing supports schema pruning with ZenStack-style model, operation, procedure, and filter slicing, plus GraphQL field visibility pruning for role-specific schemas
  • createZenStackGraphQLSchemaFactory caches one generated schema per slice key, which makes role-aware introspection and execution much easier
  • ZModel procedure and mutation procedure definitions are exposed as GraphQL query and mutation roots via client.$procs
  • extensions.query and extensions.mutation let you attach manual GraphQL root fields that receive the same request-scoped ZenStack client as generated resolvers
  • *_by_pk roots are emitted only for real primary keys
  • Relation aggregate order_by on parent collections is currently supported only for count, matching the documented ORM orderBy: { relation: { _count: ... } } shape
  • distinct_on is generated only for providers where the ORM supports distinct
  • relay.enabled adds an opt-in Relay query layer with <models>_connection, nested <relation>_connection, and node(id:)

For closer compatibility with existing Hasura documents, the easiest path is:

  • compatibility: 'hasura-compat'
    • Turns on the safe Hasura-oriented compatibility bundle:
      • singular table-style roots from model.dbName / model.name
      • Hasura/Postgres scalar aliases like uuid, timestamptz, jsonb, numeric, bigint, and citext
      • Hasura-style generated helper/input type names like payment_payable_bool_exp and uuid_comparison_exp
      • ORM-backed relation aggregate count predicates like posts_aggregate: { count: { predicate: { _eq: 0 } } }

If you only want part of that behavior, the lower-level knobs are still available:

  • naming: 'hasura-table'
    • Uses singular table-root names from model.dbName / model.name, such as identity_organization, identity_organization_by_pk, and insert_identity_organization_one
  • scalarAliases: 'hasura'
    • Renames the generated GraphQL scalar surface to Hasura/Postgres-style names where safe:
      • DateTime -> timestamptz
      • Decimal -> numeric
      • Json -> jsonb
      • BigInt -> bigint
      • native DB hints like @db.Uuid -> uuid and @db.Citext -> citext

Example:

const schema = createZenStackGraphQLSchema({
    schema: zenstackSchema,
    compatibility: 'hasura-compat',
    async getClient(context) {
        return context.db;
    },
});

Server Adapters

The low-level schema factory is still available, but the package now also includes a ZenStack-style api handler + server adapter layer so you can integrate GraphQL the same way ZenStack's REST and RPC services integrate with different server frameworks.

Use GraphQLApiHandler when you want a framework-agnostic transport boundary:

import { GraphQLApiHandler } from 'zenstack-graphql';

const handler = new GraphQLApiHandler({
    schema,
});

const response = await handler.handleRequest({
    client: db,
    method: 'POST',
    path: '/api/graphql',
    requestBody: {
        query: 'query { users { id name } }',
    },
});

GraphQLApiHandler is intentionally shaped to be assignment-compatible with ZenStack's ApiHandler type from @zenstackhq/server/api, so it can participate in the same logical handler/server-adapter model.

Then use a thin framework adapter to resolve the request-scoped client.

When a ZenStack server adapter already fits your GraphQL route shape, you can use it directly with GraphQLApiHandler instead of going through this package's fetch helper.

Next.js

import type { NextRequest } from 'next/server';
import { GraphQLApiHandler } from 'zenstack-graphql/server';
import { NextRequestHandler } from '@zenstackhq/server/next';

const apiHandler = new GraphQLApiHandler({
    schema,
    allowedPaths: [''],
});

const handler = NextRequestHandler({
    apiHandler,
    async getClient(request) {
        return getZenStackClientFromRequest(request);
    },
    useAppDir: true,
});

type RouteContext = {
    params: Promise<{ path?: string[] }>;
};

export const POST = (request: NextRequest, context: RouteContext) =>
    handler(request, {
        params: context.params.then((params) => ({
            path: params.path ?? [],
        })),
    });

Mount that handler in an optional catch-all route like app/api/graphql/[[...path]]/route.ts. The tiny params.path ?? [] normalization is only there because ZenStack's current Next.js adapter expects catch-all path params, while the root GraphQL endpoint does not supply one.

Express

import express from 'express';
import { GraphQLApiHandler } from 'zenstack-graphql/server';
import { ZenStackMiddleware } from '@zenstackhq/server/express';

const app = express();
app.use(express.json());
const graphqlApiHandler = new GraphQLApiHandler({ schema });

app.use(
    '/api/graphql',
    ZenStackMiddleware({
        apiHandler: graphqlApiHandler,
        async getClient(req) {
            return getZenStackClientFromRequest(req);
        },
    })
);

For that direct adapter path, install @zenstackhq/server alongside zenstack-graphql.

Hono

import { Hono } from 'hono';
import { GraphQLApiHandler } from 'zenstack-graphql/server';
import { createHonoHandler } from '@zenstackhq/server/hono';

const app = new Hono();
const apiHandler = new GraphQLApiHandler({ schema });
const graphql = createHonoHandler({
    apiHandler,
    async getClient(c) {
        return getZenStackClientFromRequest(c);
    },
});

app.use('/api/graphql/*', graphql);

TanStack Start

import { createFileRoute } from '@tanstack/react-router';
import { GraphQLApiHandler } from 'zenstack-graphql/server';
import { TanStackStartHandler } from '@zenstackhq/server/tanstack-start';

const apiHandler = new GraphQLApiHandler({
    schema,
    allowedPaths: ['graphql'],
});

const handler = TanStackStartHandler({
    apiHandler,
    async getClient(request) {
        return getZenStackClientFromRequest(request);
    },
});

export const Route = createFileRoute('/api/$')({
    server: {
        handlers: {
            GET: handler,
            POST: handler,
            PUT: handler,
            PATCH: handler,
            DELETE: handler,
        },
    },
});

Transport Notes

The current adapter layer supports:

  • the framework-agnostic GraphQLApiHandler
  • fetch / Web Request handlers
  • direct use with ZenStack's Express, Next.js, Hono, and TanStack Start adapters

All of them share the same core execution path, including request-wide mutation transactions, Relay support, procedures, extensions, and role-aware schema slicing.

For fixed GraphQL endpoints, the framework-specific adapters that rely on catch-all routing should be mounted on catch-all-style routes and paired with allowedPaths in GraphQLApiHandler:

  • Next.js: app/api/graphql/[[...path]]/route.ts with allowedPaths: ['']
  • Hono: app.use('/api/graphql/*', ...) with allowedPaths: ['']
  • TanStack Start: createFileRoute('/api/$') with allowedPaths: ['graphql']

Hasura Helpers

If you want a lightweight compatibility layer for Hasura-style role headers, use createHasuraCompatibilityHelpers.

import { createHasuraCompatibilityHelpers } from 'zenstack-graphql/hasura';

const hasura = createHasuraCompatibilityHelpers<Request, 'admin' | 'user'>({
    defaultRole: 'admin',
    getHeaders(request) {
        return request.headers;
    },
    normalizeRole(role) {
        return role?.toLowerCase() === 'user' ? 'user' : 'admin';
    },
    getSlicing(role) {
        return role === 'user'
            ? {
                  models: {
                      user: {
                          excludedFields: ['age'],
                      },
                  },
              }
            : undefined;
    },
});

type RequestScopedClient = ReturnType<typeof getZenStackClientFromRequest> & {
    __graphqlRole?: 'admin' | 'user';
};

const apiHandler = new GraphQLApiHandler<RequestScopedClient>({
    schema,
    getSlicing(request) {
        return hasura.getSlicing(new Request('http://local.invalid'), {
            role: request.client.__graphqlRole ?? 'admin',
        });
    },
    getCacheKey({ request }) {
        return hasura.getCacheKey({
            context: { role: request.client.__graphqlRole ?? 'admin' },
        });
    },
});

createFetchGraphQLHandler({
    apiHandler,
    async getClient(request) {
        const baseClient = await getZenStackClientFromRequest(request);
        const role = hasura.getContext(request).role;
        return new Proxy(baseClient as RequestScopedClient, {
            get(target, property, receiver) {
                if (property === '__graphqlRole') {
                    return role;
                }
                const value = Reflect.get(target, property, receiver);
                return typeof value === 'function' ? value.bind(target) : value;
            },
        });
    },
});

That helper intentionally stays small. It standardizes:

  • the x-hasura-role header name
  • role extraction from Headers or Node-style header objects
  • default-role fallback
  • request-to-context mapping
  • role-based schema slicing and cache keys

Hasura Importer

For one-off migrations, the repo now includes a CLI that can turn a Hasura Postgres metadata export plus live Postgres introspection into a best-effort schema.zmodel.

npm run hasura:import -- \
  --metadata-dir /path/to/hasura/metadata \
  --database-url "$DATABASE_URL" \
  --source default \
  --out ./schema.zmodel \
  --report

V1 importer scope:

  • Hasura Postgres metadata only
  • tracked tables and tracked views
  • best-effort ZenStack @@allow policy generation
  • inline TODO comments for unsupported permission features and no-key views

Compatibility Snapshot

This adapter is aiming for "mostly painless for common Hasura CRUD use cases", not full Hasura platform parity.

Supported well today:

  • Hasura-like list, *_by_pk, and *_aggregate query roots
  • Optional compatibility: 'hasura-compat' preset for table-style roots, Hasura/Postgres scalar aliases, Hasura-style generated helper/input type names, and safe aggregate count.predicate compatibility
  • Optional naming: 'hasura-table' mode for singular table-root compatibility with existing Hasura documents
  • Core insert, update, and delete mutation roots with returning
  • on_conflict on insert_* and insert_*_one
  • Nested relation inserts and the supported nested relation update shapes exposed by ZenStack ORM
  • Aggregates, relation aggregate fields, and parent order_by by relation count
  • Hasura-style filtering and ordering, including _between, relation some / every / none, and provider-gated distinct_on
  • ORM-backed Hasura aggregate count predicates like _eq: 0 and _gt: 0 on <relation>_aggregate.count
  • Optional scalarAliases: 'hasura' mode for Hasura/Postgres scalar names like uuid, timestamptz, jsonb, numeric, bigint, and citext
  • ZenStack custom procedures as GraphQL roots
  • Manual custom root resolvers through extensions
  • Role-aware schema pruning through slicing or the schema factory
  • Request-wide mutation transactions when the client exposes $transaction
  • Optional Relay root and nested connections plus node(id:)

Supported, but with explicit limits:

  • Relation aggregate order_by only supports count
  • Provider-specific operators only appear where ZenStack metadata says the backend supports them
  • Typed JSON / typedef filters are supported recursively for scalar, enum, typedef, and list-of-typedef fields, but not arbitrary relation fields nested inside typedefs
  • Role-aware schemas are static per slice key; auth enforcement still belongs in the ZenStack client you provide
  • Relay is implemented as a parallel type layer, so connection node objects use UserNode / PostNode types instead of reusing the existing Hasura-style User / Post object types

Intentionally unsupported right now:

  • Subscriptions
  • Hasura remote schemas
  • Auto-generated database-native SQL function/procedure roots
  • Cursor pagination
  • Relation aggregate ordering beyond ORM-backed count
  • Any feature that would require in-memory query semantics instead of safe ORM lowering

See docs/compatibility.md for the longer compatibility matrix and docs/migration.md for a practical Hasura migration checklist. Release notes for the current adapter surface are in CHANGELOG.md.

Role-aware schemas

If you want different GraphQL schemas per role, use the schema factory and derive slicing from request context.

import {
    createZenStackGraphQLSchemaFactory,
} from 'zenstack-graphql/core';

const factory = createZenStackGraphQLSchemaFactory({
    schema,
    getClient: async (context) => context.db,
    getSlicing(context) {
        return context.role === 'admin'
            ? undefined
            : {
                  models: {
                      user: {
                          excludedFields: ['age'],
                          excludedOperations: ['deleteMany', 'deleteByPk'],
                      },
                  },
              };
    },
    getCacheKey({ context }) {
        return context.role;
    },
});

const graphqlSchema = await factory.getSchema(context);
const result = await factory.execute({
    contextValue: context,
    source: '{ users { id name } }',
});

Notes

  • The adapter accepts a normalized metadata object today so it can work as a standalone package before being wired into a full ZenStack V3 repository.
  • Delegates are expected to look Prisma-like (findMany, findUnique, aggregate, create, update, delete, and optional bulk variants).
  • Provider capabilities are normalized from the schema metadata so backend-specific filter behavior can be gated cleanly as the adapter grows.
  • ZenStack custom procedures are supported; database-native SQL routines are not auto-generated today.
  • The root zenstack-graphql entrypoint is a convenience export; framework-specific subpaths are the cleaner long-term import surface for apps and examples.

Example Apps

The repository now includes four runnable examples:

  • examples/nextjs-demo
    • Full browser playground with schema viewer, seeded data panel, role switching, and sample operations
  • examples/express-demo
    • Minimal Express server using ZenStack's ZenStackMiddleware with GraphQLApiHandler
  • examples/hono-demo
    • Minimal Hono server using ZenStack's createHonoHandler with GraphQLApiHandler
  • examples/tanstack-start-demo
    • TanStack Start app using ZenStack's TanStackStartHandler with GraphQLApiHandler

All four examples use a real ZenStack schema, generate local metadata with zenstack generate, boot a SQLite database, and support Hasura-style role selection via the x-hasura-role header.

Next.js

The Next.js playground includes examples for:

  • Nested reads and aggregates
  • CRUD mutations, nested inserts, and on_conflict
  • Atomic rollback across multiple mutation fields
  • JSON-path filters and _between
  • Relay root connections, nested relation connections, and node(id:)
  • ZenStack procedures and manual extension roots
  • Role-pruned schemas with x-hasura-role
cd examples/nextjs-demo
npm install
npm run dev

Or from the repo root:

npm run demo:dev

Express

cd examples/express-demo
npm install
npm run dev

Or from the repo root:

npm run demo:express:dev

Hono

cd examples/hono-demo
npm install
npm run dev

Or from the repo root:

npm run demo:hono:dev

TanStack Start

cd examples/tanstack-start-demo
npm install
npm run dev

Or from the repo root:

npm run demo:tanstack:dev