zenstack-orpc
v0.2.1
Published
ZenStack plugin for generating oRPC routers with automatic Row Level Security support
Downloads
8
Maintainers
Readme
zenstack-orpc
ZenStack plugin for generating oRPC routers with automatic Row Level Security support.
🎯 Features
- ✅ Generate oRPC routers directly from ZenStack schema
- ✅ Full Row Level Security (RLS) policy support
- ✅ Automatic validation via Zod schemas
- ✅ Type-safe API with complete type inference
- ✅ Access error handling (P2004)
- ✅ Support for all Prisma CRUD operations
- ✅ Simplified architecture (no adapter layer needed)
📦 Installation
npm install -D zenstack-orpc
# or
pnpm add -D zenstack-orpc
# or
yarn add -D zenstack-orpc🚀 Usage
1. Add plugin to schema.zmodel
plugin orpc {
provider = 'zenstack-orpc'
output = 'zenstack/orpc'
}2. Run generation
npx zenstack generate3. Use generated routers
import { appRouter } from './zenstack/orpc/routers';
import { createORPCHandler } from '@orpc/server';
import { PrismaClient } from '@prisma/client';
const handler = createORPCHandler({
router: appRouter,
context: async (req) => ({
prisma: new PrismaClient(),
user: await getUserFromRequest(req),
}),
});📁 Generated Structure
zenstack/orpc/
├── routers/
│ ├── index.ts # Main router (appRouter)
│ ├── User.router.ts
│ ├── Post.router.ts
│ └── ... # Router for each model
└── helper.ts # Utilities and context🔧 Configuration
Plugin Options
plugin orpc {
provider = 'zenstack-orpc'
output = 'zenstack/orpc' // Output path
generateModels = ['User', 'Post'] // Generate only specified models
generateModelActions = ['findMany', 'create'] // Generate only specified operations
zodSchemasImport = '../../zod' // Path to Zod schemas import
baseImport = '../base' // Import base context from custom path
}Custom Base Context
By default, the plugin generates a basic context. You can provide your own:
// base.ts
import { os } from '@orpc/server';
import type { PrismaClient } from '@prisma/client';
export const base = os.$context<{
prisma: PrismaClient;
user?: {
id: string;
role: string;
// Add your custom fields
};
}>();Then use baseImport option:
plugin orpc {
provider = 'zenstack-orpc'
output = 'zenstack/orpc'
baseImport = '../base'
}📚 API
Context
The plugin generates a typed context:
type Context = {
prisma: PrismaClient;
user?: {
id: string;
role: string;
};
};Operations
For each model, the following operations are generated:
Queries (8 operations):
aggregate- aggregate datacount- count recordsfindFirst- find first recordfindFirstOrThrow- find first record (throws if not found)findMany- find multiple recordsfindUnique- find unique recordfindUniqueOrThrow- find unique record (throws if not found)groupBy- group data
Mutations (7 operations):
create- create recordcreateMany- create multiple recordsdelete- delete recorddeleteMany- delete multiple recordsupdate- update recordupdateMany- update multiple recordsupsert- create or update record
Helper Functions
// Check read operations
export async function checkRead<T>(promise: Promise<T>): Promise<T>
// Check write operations
export async function checkMutate<T>(promise: Promise<T>): Promise<T | undefined>🔒 Row Level Security (RLS)
The plugin automatically integrates with ZenStack RLS policies:
model Post {
id String @id @default(cuid())
title String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
// Anyone can read published posts
@@allow('read', published)
// Only authenticated users can read all posts
@@allow('read', auth() != null)
// Only author can update/delete their posts
@@allow('update,delete', auth() == author)
// Admins have full access
@@allow('all', auth().role == 'admin')
}Access errors are automatically handled:
// Prisma error P2004 → "Access denied"
// Prisma error P2025 → "Not found"🆚 Comparison with tRPC
| Feature | tRPC | oRPC | |---------|------|------| | Type Safety | ✅ | ✅ | | RLS Support | Via ZenStack | Via ZenStack | | Bundle Size | Larger | Smaller | | Learning Curve | Steeper | Gentler | | Middleware | Complex | Simple |
🏗️ Architecture
Generated Router for Model
// Generated code
export const PostRouter = {
findMany: base
.input($Schema.PostInputSchema.findMany)
.handler(async ({ context, input }) =>
checkRead(context.prisma.post.findMany(input))
),
create: base
.input($Schema.PostInputSchema.create)
.handler(async ({ context, input }) =>
checkMutate(context.prisma.post.create(input))
),
// ... +13 operations
};Main Router
export const appRouter = {
user: UserRouter,
post: PostRouter,
// ... all models
};
export type AppRouter = typeof appRouter;📖 Examples
Simple Query
const posts = await orpcClient.post.findMany({
where: { published: true },
take: 10,
});Mutation with RLS
// RLS automatically checks permissions
const post = await orpcClient.post.create({
data: {
title: 'Hello World',
published: false,
authorId: userId,
},
});Error Handling
try {
await orpcClient.post.delete({
where: { id: 'post-id' }
});
} catch (err) {
if (err.message === 'Access denied') {
// RLS denied the operation
}
}🤝 Contributing
Contributions are welcome! Please open an issue or PR.
📝 License
MIT
