Skip to main content

Query builder

biref.query<BirefSchema>(model) returns a two-layer Proxy keyed by namespace then entity. Every leaf is a fresh TypedChain.

const q = biref.query<BirefSchema>(model);

const users = q.public.users; // TypedChain bound to public.users
const products = q.catalog.products; // TypedChain bound to catalog.products

The chain is immutable -- every method returns a new chain.

select()

Project a subset of columns:

// Specific fields (narrows the return type)
q.public.users.select('id', 'email');

// Wildcard (all fields)
q.public.users.select('*');

// Zero-arg shorthand for wildcard
q.public.users.select();

Rules:

  • Every named field must exist on the entity. Unknown names throw at chain time.
  • Mixing '*' with named fields throws.

where()

Filter by predicate. Operators narrow automatically based on the field's type category:

q.public.users
.where('status', 'eq', 'active')
.where('age', 'gte', 18)
.where('email', 'ilike', '%@example.com')
.where('deleted_at', 'is-null')
.where('role', 'in', ['admin', 'editor']);

Operators by category

CategoryOperators
string, uuid, enumeq, neq, in, not-in, like, ilike, is-null, is-not-null
integer, decimal, date, timestamp, timeeq, neq, lt, lte, gt, gte, between, in, not-in, is-null, is-not-null
booleaneq, neq, is-null, is-not-null

Value shapes

OperatorValue
eq, neq, lt, lte, gt, gte, like, ilikescalar
in, not-inreadonly T[]
betweenreadonly [min, max]
is-null, is-not-nullno value (two-arg form)

orderBy()

q.public.orders
.orderBy('placed_at', 'desc')
.orderBy('id', 'asc');

Direction defaults to 'asc'.

limit() / offset()

q.public.products
.limit(10)
.offset(20);

include()

Attach related entities as nested results:

// Shorthand (default projection)
q.public.users
.select('id', 'email')
.include('orders');

// With sub-builder (narrowed projection)
q.public.users
.select('id', 'email')
.include('orders', (order) =>
order
.select('id', 'total', 'status')
.where('status', 'neq', 'cancelled')
.include('order_items', (item) =>
item.select('product_id', 'quantity'),
),
);

// Wildcard (every relation, default projection)
q.public.users
.select('id', 'email')
.include('*');

Include works in both directions:

  • .include('orders') on users walks the inbound hop (orders reference users)
  • .include('user') on orders walks the outbound hop (orders declare the FK)

Cardinality:

  • many (inbound): result is readonly T[]
  • one (outbound): result is T | null

Terminal methods

// All matching rows
const rows = await q.public.users.select('id', 'email').findMany();

// First match (applies LIMIT 1)
const row = await q.public.users
.select('id', 'email')
.where('email', 'eq', 'alice@example.com')
.findFirst();
// Returns T | null

JavaScript usage (no codegen)

Skip the schema generic and everything still works with dynamic types:

const q = biref.query(model);
const rows = await q.public.users.select('id', 'email').findMany();
// rows is readonly Record<string, ParsedValue>[]