oRPC
A UQL query is plain JSON, so it can be an oRPC procedure’s input with no adapter. Use oRPC for per-procedure contracts, and the HTTP handler for CRUD across many entities; both are fetch-native and mount side by side.
Router
Section titled “Router”oRPC’s type<T>() declares the input type:
import { os, type } from '@orpc/server';import type { EntityData, Type, WireQuery } from 'uql-orm';import { User } from './shared/models/index.js';import { pool } from './uql.config.js';
function entityRouter<E extends object>(entity: Type<E>) { return { findMany: os .input(type<WireQuery<E>>()) .handler(({ input }) => pool.findMany(entity, input)), insertOne: os // type<T>() alone cannot resolve a mapped type over a generic, so pass the mapper .input(type<EntityData<E>>((value) => value)) .handler(({ input }) => pool.insertOne(entity, input)), };}
export const router = { user: entityRouter(User) };The client gets the whole query typed, nested relations included:
import { createORPCClient } from '@orpc/client';import { RPCLink } from '@orpc/client/fetch';import type { RouterClient } from '@orpc/server';import type { router } from './router.js';
const client: RouterClient<typeof router> = createORPCClient( new RPCLink({ url: '/rpc' }),);
const users = await client.user.findMany({ $select: { id: true, name: true }, $populate: { posts: { $select: { title: true }, $limit: 5 } }, $limit: 10,});// typed User[], each with a typed posts: Post[]Validating inputs
Section titled “Validating inputs”type<T>() declares a type and checks nothing at runtime. For procedures an untrusted client can call, validate a few arguments with a schema such as zod and build the query on the server. oRPC’s OpenAPI generator also skips type-only inputs.
For tenant isolation, run the handler inside withContext({ tenantId: ctx.session.tenantId }, () => ...), with the id from a verified session, and declare a security filter. Unlike a $where added by hand, the filter reaches every relation and fails closed. See Multi-tenancy.