Querying
Read Rayfin entities with the type-safe select/where/orderBy/execute chain, including filtering, sorting, and cursor pagination.
RayfinClient exposes a fluent, typed query builder for every entity in your schema at
client.data.<Entity>. It compiles to GraphQL against Data API Builder — you never write
a query string by hand.
Set up the client
Every example below uses the shared rayfinClient — see
Set up the client for how to construct it once and reuse it.
The query chain
Build a query by chaining .select(), then optionally .where() and .orderBy(), then
call .execute():
const todos = await rayfinClient.data.Todo.select([
'id',
'title',
'isCompleted',
'createdAt',
])
.where({ isCompleted: { eq: false } })
.orderBy({ createdAt: 'desc' })
.execute();.select() is required — list every field your code needs. .where() and .orderBy()
are optional and can be omitted or reordered relative to each other, but .execute() (or
.executePaginated(), see Pagination) always comes last.
Warning
.execute() returns a single page — 100 records by default — and does not tell you
whether more records exist. A list that grows past one page is silently truncated. Use
.execute() only for queries you know are bounded (a lookup table, a filter that can
match only a handful of rows). For anything unbounded — a user's notes, an order
history — use pagination instead.
Fetch a single record
const todo = await rayfinClient.data.Todo.findById('00000000-0000-0000-0000-000000000000');Use findById — not findByPk. It returns the record or null if no row matches.
findFirst returns the first record matching an optional filter, or null:
const notebook = await rayfinClient.data.Notebook.findFirst({ name: { eq: 'Work' } });findMany runs a filtered query in one call without building a chain, equivalent to
.select([...]).where(filter).execute() for cases where you want every field:
const active = await rayfinClient.data.Todo.findMany({ isCompleted: { eq: false } });Filter with .where()
.where() takes an object keyed by field name. A bare value is shorthand for eq; an
operator object is more explicit and required for anything other than equality.
.where({ isCompleted: { eq: true } })Operators by field type
| Field type | Available operators |
|---|---|
@text() / @email() | eq, neq, gt, gte, lt, lte, contains, notContains, startsWith, endsWith, isNull, in |
@int() / @decimal() | eq, neq, gt, gte, lt, lte, isNull, in |
@boolean() | eq, neq, isNull, in |
@date() | eq, neq, gt, gte, lt, lte, isNull, in |
Combine conditions
Multiple keys in one .where() object are implicitly ANDed. Use explicit and / or
arrays to combine or nest conditions:
.where({
or: [
{ title: { contains: 'urgent' } },
{ isCompleted: { eq: false } },
],
})Filter by foreign key, not by dot-path
Filter relationships by their {property}_id foreign key column, not by a dot-path into
the related entity:
.where({ notebook_id: { eq: notebookId } }) // correct
.where({ 'notebook.id': { eq: notebookId } }) // wrong — dot-paths are select-onlyFor an optional relationship, filter for rows with no related record using isNull:
.where({ notebook: { isNull: true } })Select nested fields with dot-paths
Dot-paths are for .select() only — use them to pull fields off a related entity into the
same result row:
const notes = await rayfinClient.data.Note.select([
'id',
'title',
'notebook_id',
'notebook.id',
'notebook.name',
])
.orderBy({ createdAt: 'desc' })
.execute();
// notes[0].notebook.name is available directlyNesting is supported to exactly one level past the root entity — notebook.name works,
but notebook.owner.email (a third level) does not.
Sort with .orderBy()
Sort directions are the lowercase strings 'asc' and 'desc' — not capitalized
constants:
.orderBy({ createdAt: 'desc' })Paginate large lists
Because .execute() returns only one page and gives no signal that more records exist,
use .first(n) with .executePaginated() for any query that can grow past a single page.
Fetch one page
const page = await rayfinClient.data.Note.select(['id', 'title', 'createdAt'])
.orderBy({ createdAt: 'desc' })
.first(25)
.executePaginated();
page.items; // up to 25 records
page.hasNextPage; // true if more records remain
page.endCursor; // pass to .after() to fetch the next pageFetch the next page
const nextPage = await rayfinClient.data.Note.select(['id', 'title', 'createdAt'])
.orderBy({ createdAt: 'desc' })
.first(25)
.after(page.endCursor)
.executePaginated();Keep .select(), .where(), and .orderBy() identical across every page in a sequence —
a stable sort order is required for the cursor to advance correctly.
Fetch every record
Loop, passing each page's endCursor into the next call's .after(), until
hasNextPage is false:
async function fetchAllNotes() {
const all: Array<{ id: string; title: string; createdAt: Date }> = [];
let cursor: string | undefined;
do {
const page = await rayfinClient.data.Note.select(['id', 'title', 'createdAt'])
.orderBy({ createdAt: 'desc' })
.first(100)
.after(cursor)
.executePaginated();
all.push(...page.items);
cursor = page.hasNextPage ? page.endCursor : undefined;
} while (cursor);
return all;
}Note
.first(n) is bounded by Data API Builder's maximum page size of 100,000.
.first(-1) requests an unbounded page and still hits that same cap, so it only works
when the full result set fits under it. For anything that might not, page through
results with .after() instead of requesting everything in one large .first(n).
PagedResult also exposes a totalCount field, but Data API Builder does not populate
it on paginated queries — do not rely on it.
Counting records
There is no count() on the fluent query chain. What exists is
aggregation — count is one of five operations available through
groupBy() and aggregate():
const [totals] = await rayfinClient.data.Order
.where({ status: { eq: 'open' } })
.aggregate({ open: { count: 'amount' } })
.execute();
totals.aggregations.open; // numberImportant
count aggregates numeric values, not rows. Data API Builder types every
aggregation's field argument as the entity's numeric fields, so you can only count a
numeric column — and the number it returns is the count of rows where that column is
non-null.
That makes count a true row count only when you point it at a non-nullable numeric column.
For an entity that has none — a Todo with a uuid id and text title, say — fall back to
counting client-side:
const openTodos = await rayfinClient.data.Todo.select(['id'])
.where({ isCompleted: { eq: false } })
.execute();
const openCount = openTodos.length; // only correct if the result fits in one pageFor a count that might exceed one page, page through with .executePaginated() and sum
page.items.length across pages, since .execute() truncates and totalCount is not
populated.
In my Rayfin project, write a function that fetches all Todo records for the current
user's "Archive" view using the RayfinClient. Select id, title, isCompleted, and
createdAt, filter to isCompleted: { eq: true }, order by createdAt descending, and page
through with .first(50) and .executePaginated() until hasNextPage is false, returning the
combined list.Permissions and row-level security
Secure Rayfin entities with @role, @anonymous, and @authenticated, including row-level policies, field visibility, action-specific rules, and multi-tenant scoping.
Aggregations
Compute sums, averages, minimums, maximums, and counts over your entities with groupBy() and aggregate(), including grand totals and having filters.