Rayfin

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 typeAvailable 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-only

For 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 directly

Nesting 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 page

Fetch 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 aggregationcount 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; // number

Important

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 page

For 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.

PromptAdd a paginated list query
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.
Something wrong on this page?Report an issueEdit this page

On this page