Skip to content
NewComposite primary keys5 min read

oRPC

UQL queries are plain JSON, so they pass through oRPC procedures without any adapter. There is nothing to install beyond your existing oRPC setup: procedures call the querier pool directly, and oRPC’s type<T>() helper declares the pass-through input type.

import { os, type } from '@orpc/server';
import type { Query, Type } from 'uql-orm/type';
import { pool } from './uql.config.js';
import { User } from './shared/models/index.js';
function entityRouter<E extends object>(entity: Type<E>) {
return {
findMany: os
.input(type<Query<E>>())
.handler(({ input }) => pool.findMany(entity, input)),
insertOne: os
.input(type<E>((value) => value)) // optional identity mapper; type<E>() alone also works
.handler(({ input }) => pool.insertOne(entity, input)),
};
}
export const router = { user: entityRouter(User) };

On the client the whole query is typed end to end, filters, sorting and nested relation loading alike, and reaches the server as plain JSON with no per-procedure schema to keep in sync:

const users = await client.user.findMany({
$select: { id: true, name: true },
$where: { status: 'active', email: { $endsWith: '@domain.com' } },
$populate: {
posts: { $select: { title: true }, $where: { published: true }, $limit: 5 },
},
$limit: 10,
});
// typed User[], each with a typed posts: Post[]

A handler that writes more than one row wraps its statements in pool.transaction and runs each on the callback’s querier: a pool.x call inside there takes a second connection, so it lands outside the transaction.

Prefer oRPC or tRPC when you want per-procedure contracts; prefer the HTTP core for zero-boilerplate CRUD across many entities. oRPC’s RPCHandler is fetch-native, so both mount side by side in one app, and the query object is identical either way: one query, every transport.